Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\DataTransformer;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\LeadBundle\Entity\LeadListRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @implements DataTransformerInterface<mixed, array<mixed>|mixed>
*/
class FieldFilterTransformer implements DataTransformerInterface
{
/**
* @var string[]
*/
private array $relativeDateStrings;
public function __construct(
TranslatorInterface $translator,
private array $default = [],
) {
$this->relativeDateStrings = LeadListRepository::getRelativeDateTranslationKeys();
foreach ($this->relativeDateStrings as &$string) {
$string = $translator->trans($string);
}
}
/**
* From DB format to form format.
*/
public function transform(mixed $rawFilters): mixed
{
if (!is_array($rawFilters)) {
return [];
}
foreach ($rawFilters as $key => $filter) {
if (!empty($this->default)) {
$rawFilters[$key] = array_merge($this->default, $rawFilters[$key]);
}
if ('datetime' === $filter['type']) {
$bcFilter = $filter['filter'] ?? '';
$filter = $filter['properties']['filter'] ?? $bcFilter;
if (empty($filter) || in_array($filter, $this->relativeDateStrings) || stristr($filter[0], '-') || stristr($filter[0], '+')) {
continue;
}
$dt = new DateTimeHelper($filter, 'Y-m-d H:i');
$rawFilters[$key]['properties']['filter'] = $dt->toLocalString();
}
}
return $rawFilters;
}
/**
* Form format to database format.
*/
public function reverseTransform(mixed $rawFilters): mixed
{
if (!is_array($rawFilters)) {
return [];
}
$rawFilters = array_values($rawFilters);
foreach ($rawFilters as $k => $f) {
if ('datetime' === $f['type']) {
$bcFilter = $f['filter'] ?? '';
$filter = $f['properties']['filter'] ?? $bcFilter;
if (empty($filter) || in_array($filter, $this->relativeDateStrings) || stristr($filter[0], '-') || stristr($filter[0], '+')) {
continue;
}
$dt = new DateTimeHelper($filter, 'Y-m-d H:i', 'local');
$rawFilters[$k]['properties']['filter'] = $dt->toUtcString();
}
}
return $rawFilters;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Mautic\LeadBundle\Form\DataTransformer;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Entity\LeadFieldRepository;
use Symfony\Component\Form\DataTransformerInterface;
/**
* @implements DataTransformerInterface<LeadField|null, int|null>
*/
class FieldToOrderTransformer implements DataTransformerInterface
{
public function __construct(
private LeadFieldRepository $leadFieldRepository,
) {
}
/**
* Transforms an object to an integer (order).
*
* @param int|null $order
*
* @return LeadField|null
*/
public function transform(mixed $order): mixed
{
if (!$order) {
return null;
}
return $this->leadFieldRepository->findOneBy(['order' => $order]);
}
/**
* Transforms a integer to an object.
*
* @param LeadField|null $field
*
* @return int|null
*/
public function reverseTransform(mixed $field): mixed
{
if (null === $field) {
return null;
}
return $field->getOrder();
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Mautic\LeadBundle\Form\DataTransformer;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @implements DataTransformerInterface<array<mixed>|int|null, array<mixed>|object|null>
*/
class TagEntityModelTransformer implements DataTransformerInterface
{
/**
* @param string $repository
* @param bool $isArray
*/
public function __construct(
private EntityManager $em,
private $repository = '',
private $isArray = false,
) {
}
public function reverseTransform(mixed $entity): mixed
{
if (!$this->isArray) {
if (is_null($entity) || !is_object($entity)) {
return null;
}
return $entity->getTag();
}
if (is_null($entity) && !is_array($entity) && !$entity instanceof PersistentCollection) {
return [];
}
$return = [];
foreach ($entity as $e) {
$return[] = $e->getTag();
}
return $return;
}
/**
* @throws TransformationFailedException if object is not found
*/
public function transform(mixed $id): mixed
{
if (!$this->isArray) {
if (!$id) {
return null;
}
$column = (is_numeric($id)) ? 'id' : 'tag';
$entity = $this->em
->getRepository($this->repository)
->findOneBy([$column => $id]);
if (null === $entity) {
throw new TransformationFailedException(sprintf('Tag with "%s" does not exist!', $id));
}
return $entity;
}
if (empty($id) || !is_array($id)) {
return [];
}
$column = (is_numeric($id[0])) ? 'id' : 'tag';
$repo = $this->em->getRepository($this->repository);
$prefix = $repo->getTableAlias();
$entities = $repo->getEntities([
'filter' => [
'force' => [
[
'column' => $prefix.'.'.$column,
'expr' => 'in',
'value' => $id,
],
],
],
'ignore_paginator' => true,
]);
if (!count($entities)) {
throw new TransformationFailedException(sprintf('Tags for "%s" does not exist!', implode(', ', $id)));
}
return $entities;
}
/**
* Set the repository to use.
*
* @param string $repo
*/
public function setRepository($repo): void
{
$this->repository = $repo;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Mautic\LeadBundle\Form;
use Mautic\CoreBundle\Form\Type\BooleanType;
use Mautic\CoreBundle\Form\Type\CountryType;
use Mautic\CoreBundle\Form\Type\LocaleType;
use Mautic\CoreBundle\Form\Type\LookupType;
use Mautic\CoreBundle\Form\Type\MultiselectType;
use Mautic\CoreBundle\Form\Type\RegionType;
use Mautic\CoreBundle\Form\Type\SelectType;
use Mautic\CoreBundle\Form\Type\TelType;
use Mautic\CoreBundle\Form\Type\TimezoneType;
use Mautic\LeadBundle\Exception\FieldNotFoundException;
use Mautic\LeadBundle\Form\Type\HtmlType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
/**
* Provides map between Mautic 2 (Symfony 2.8) form aliases and Mautic 3 (Symfony 3.4) FQCN.
*/
final class FieldAliasToFqcnMap
{
/**
* @format [field alias => field FQCN]
*/
public const MAP = [
'boolean' => BooleanType::class,
'country' => CountryType::class,
'date' => DateType::class,
'datetime' => DateTimeType::class,
'email' => EmailType::class,
'hidden' => HiddenType::class,
'locale' => LocaleType::class,
'lookup' => LookupType::class,
'multiselect' => MultiselectType::class,
'number' => NumberType::class,
'region' => RegionType::class,
'select' => SelectType::class,
'tel' => TelType::class,
'text' => TextType::class,
'textarea' => TextareaType::class,
'time' => TimeType::class,
'timezone' => TimezoneType::class,
'url' => UrlType::class,
'html' => HtmlType::class,
];
public static function getFqcn(string $alias): string
{
if (array_key_exists($alias, self::MAP)) {
return self::MAP[$alias];
}
throw new FieldNotFoundException("Field with alias {$alias} not found");
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
/**
* @extends AbstractType<mixed>
*/
class ActionAddUtmTagsType extends AbstractType
{
public function getBlockPrefix(): string
{
return 'lead_action_addutmtags';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
/**
* @extends AbstractType<mixed>
*/
class ActionRemoveDoNotContact extends AbstractType
{
public function getBlockPrefix(): string
{
return 'lead_action_removedonotcontact';
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class AddToCompanyActionType extends AbstractType
{
public function __construct(
protected RouterInterface $router,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'company',
CompanyListType::class,
[
'multiple' => false,
'required' => true,
'modal_route' => false,
'constraints' => [
new NotBlank(
['message' => 'mautic.company.choosecompany.notblank']
),
],
]
);
$windowUrl = $this->router->generate(
'mautic_company_action',
[
'objectAction' => 'new',
'contentOnly' => 1,
'updateSelect' => 'campaignevent_properties_company',
]
);
$builder->add(
'newCompanyButton',
ButtonType::class,
[
'attr' => [
'class' => 'btn btn-primary btn-nospin',
'onclick' => 'Mautic.loadNewWindow({"windowUrl": "'.$windowUrl.'"})',
'icon' => 'ri-add-line',
],
'label' => 'mautic.company.new.company',
]
);
}
public function getBlockPrefix(): string
{
return 'addtocompany_action';
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class BatchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'add',
ChoiceType::class,
[
'label' => 'mautic.lead.batch.add_to',
'multiple' => true,
'choices' => $options['items'],
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'remove',
ChoiceType::class,
[
'label' => 'mautic.lead.batch.remove_from',
'multiple' => true,
'choices' => $options['items'],
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add('ids', HiddenType::class);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(
[
'items',
]
);
}
public function getBlockPrefix(): string
{
return 'lead_batch';
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class CampaignActionAddDNCType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'channels',
PreferenceChannelsType::class,
[
'label' => 'mautic.lead.contact.channels',
'multiple' => true,
'required' => true,
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'reason',
TextareaType::class,
[
'label' => 'mautic.lead.batch.dnc_reason',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class CampaignActionRemoveDNCType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'channels',
PreferenceChannelsType::class,
[
'label' => 'mautic.lead.contact.channels',
'multiple' => true,
'required' => true,
'constraints' => [
new NotBlank(),
],
]
);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\DataTransformer\SecondsConversionTransformer;
use Mautic\PageBundle\Form\Type\PageListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignConditionLeadPageHitType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('page_url', TextType::class, [
'label' => 'mautic.page.point.action.form.page.url',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.page.point.action.form.page.url.descr',
'placeholder' => 'https://',
],
'required' => false,
]);
$builder->add('page', PageListType::class, [
'label' => 'mautic.page.campaign.condition.form.page',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.page.campaign.condition.form.page.descr',
],
'multiple' => false,
'required' => false,
'placeholder' => 'Choose Page',
]);
$builder->add(
'startDate',
TextType::class,
[
'label' => 'mautic.page.campaign.condition.form.startdate',
'attr' => [
'class' => 'form-control',
'preaddon' => 'ri-calendar-line',
'data-toggle' => 'datetime',
],
'required' => false,
]
);
$builder->add(
'endDate',
TextType::class,
[
'label' => 'mautic.page.campaign.condition.form.enddate',
'attr' => [
'class' => 'form-control',
'preaddon' => 'ri-calendar-line',
'data-toggle' => 'datetime',
],
'required' => false,
]
);
$formModifier = function (FormInterface $form, $data) use ($builder): void {
$unit = 's';
$form->add('accumulative_time_unit', HiddenType::class, [
'data' => $unit,
]);
$secondsTransformer = new SecondsConversionTransformer($unit);
$form->add(
$builder->create('accumulative_time', TextType::class, [
'label' => 'mautic.page.campaign.condition.form.timespent',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'auto_initialize' => false,
])
->addViewTransformer($secondsTransformer)
->getForm()
);
$unit = 's';
$secondsTransformer = new SecondsConversionTransformer($unit);
$form->add('returns_within_unit', HiddenType::class, [
'data' => $unit,
]);
};
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier): void {
$data = $event->getData();
$formModifier($event->getForm(), $data);
}
);
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier): void {
$data = $event->getData();
$formModifier($event->getForm(), $data);
}
);
}
public function getBlockPrefix(): string
{
return 'campaigncondition_lead_pageHit';
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CampaignBundle\Form\Type\CampaignListType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\LeadBundle\Model\ListModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadCampaignsType extends AbstractType
{
public function __construct(
protected ListModel $listModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('campaigns',
CampaignListType::class, [
'label' => 'mautic.lead.lead.events.campaigns.membership',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => true,
]);
$builder->add(
'dataAddedLimit',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.lead.events.campaigns.date.added.filter',
'data' => $options['data']['dataAddedLimit'] ?? false,
]
);
$builder->add(
'expr',
ChoiceType::class,
[
'label' => 'mautic.lead.lead.events.campaigns.expression',
'multiple' => false,
'choices' => $this->listModel->getOperatorsForFieldType([
'include' => [
'gt',
'lt',
],
]),
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'data-show-on' => '{"campaignevent_properties_dataAddedLimit_1":"checked"}',
],
]
);
$builder->add(
'dateAdded',
TextType::class,
[
'label' => 'mautic.lead.lead.events.campaigns.date',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'data-toggle' => 'datetime',
'data-show-on' => '{"campaignevent_properties_dataAddedLimit_1":"checked"}',
],
'required' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_campaigns';
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Entity\DoNotContact;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadDNCType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'channels',
PreferenceChannelsType::class,
[
'label' => 'mautic.lead.contact.channels',
'multiple' => true,
'required' => true,
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'reason',
ChoiceType::class,
[
'choices' => [
'mautic.lead.do.not.contact_bounced' => DoNotContact::BOUNCED,
'mautic.lead.do.not.contact_unsubscribed' => DoNotContact::UNSUBSCRIBED,
'mautic.lead.do.not.contact_manual' => DoNotContact::MANUAL,
],
'label' => 'mautic.lead.batch.dnc_reason',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use DeviceDetector\Parser\Device\AbstractDeviceParser as DeviceParser;
use DeviceDetector\Parser\OperatingSystem;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadDeviceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'device_type',
ChoiceType::class,
[
'label' => 'mautic.lead.campaign.event.device_type',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'choices' => array_combine(DeviceParser::getAvailableDeviceTypeNames(), DeviceParser::getAvailableDeviceTypeNames()),
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
$builder->add(
'device_brand',
ChoiceType::class,
[
'label' => 'mautic.lead.campaign.event.device_brand',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'choices' => array_flip(DeviceParser::$deviceBrands),
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
$builder->add(
'device_os',
ChoiceType::class,
[
'label' => 'mautic.lead.campaign.event.device_os',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'choices' => array_combine(array_keys(OperatingSystem::getAvailableOperatingSystemFamilies()), array_keys(OperatingSystem::getAvailableOperatingSystemFamilies())),
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_device';
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Helper\ArrayHelper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Helper\FormFieldHelper;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Segment\OperatorOptions;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadFieldValueType extends AbstractType
{
public function __construct(
protected Translator $translator,
protected LeadModel $leadModel,
protected FieldModel $fieldModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'field',
LeadFieldsType::class,
[
'label' => 'mautic.lead.campaign.event.field',
'label_attr' => ['class' => 'control-label'],
'multiple' => false,
'with_company_fields' => true,
'with_tags' => true,
'with_utm' => true,
'placeholder' => 'mautic.core.select',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.campaign.event.field_descr',
'onchange' => 'Mautic.updateLeadFieldValues(this)',
],
'required' => true,
'constraints' => [
new NotBlank(
['message' => 'mautic.core.value.required']
),
],
]
);
// function to add 'template' choice field dynamically
$func = function (FormEvent $e): void {
$data = $e->getData();
$form = $e->getForm();
$fieldValues = null;
$fieldType = null;
$choiceAttr = [];
$operator = '=';
if (isset($data['field'])) {
$field = $this->fieldModel->getRepository()->findOneBy(['alias' => $data['field']]);
$operator = $data['operator'];
if ($field) {
$properties = $field->getProperties();
$fieldType = $field->getType();
if (!empty($properties['list'])) {
// Lookup/Select options
$fieldValues = FormFieldHelper::parseList($properties['list']);
} elseif (!empty($properties) && 'boolean' == $fieldType) {
// Boolean options
$fieldValues = [
0 => $properties['no'],
1 => $properties['yes'],
];
} else {
switch ($fieldType) {
case 'country':
$fieldValues = FormFieldHelper::getCountryChoices();
break;
case 'region':
$fieldValues = ArrayHelper::flatten(FormFieldHelper::getRegionChoices());
break;
case 'timezone':
$fieldValues = ArrayHelper::flatten(FormFieldHelper::getTimezonesChoices());
break;
case 'locale':
// Locales are flipped. And yes, we will flip the array again below.
$fieldValues = array_flip(FormFieldHelper::getLocaleChoices());
break;
case 'date':
case 'datetime':
if ('date' === $operator) {
$fieldHelper = new FormFieldHelper();
$fieldHelper->setTranslator($this->translator);
$fieldValues = $fieldHelper->getDateChoices();
$customText = $this->translator->trans('mautic.campaign.event.timed.choice.custom');
$customValue = (empty($data['value']) || isset($fieldValues[$data['value']])) ? 'custom' : $data['value'];
$fieldValues = array_merge(
[
$customValue => $customText,
],
$fieldValues
);
$choiceAttr = function ($value, $key, $index) use ($customValue): array {
if ($customValue === $value) {
return ['data-custom' => 1];
}
return [];
};
}
break;
case 'boolean':
case 'lookup':
case 'select':
case 'radio':
if (!empty($properties)) {
$fieldValues = $properties;
}
}
}
}
}
$supportsValue = !in_array($operator, ['empty', '!empty']);
$supportsChoices = !in_array($operator, ['empty', '!empty', 'regexp', '!regexp']);
// Display selectbox for a field with choices, textbox for others
if (!empty($fieldValues) && $supportsChoices) {
$isMultiple = in_array($operator, [OperatorOptions::INCLUDING_ANY, OperatorOptions::EXCLUDING_ANY]);
$value = $isMultiple && !is_array($data['value']) ? [$data['value']] : $data['value'];
$innerBuilder = $form->getConfig()->getFormFactory()->createNamedBuilder('value', ChoiceType::class, null, [
'choices' => array_flip($fieldValues),
'label' => 'mautic.form.field.form.value',
'multiple' => $isMultiple,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'onchange' => 'Mautic.updateLeadFieldValueOptions(this)',
'data-toggle' => $fieldType,
'data-onload-callback' => 'updateLeadFieldValueOptions',
],
'choice_attr' => $choiceAttr,
'required' => true,
'constraints' => [
new NotBlank(
['message' => 'mautic.core.value.required']
),
],
'auto_initialize' => false,
'data' => $value,
]);
$transform = function ($value) use ($isMultiple) {
if ($isMultiple) {
return !is_array($value) ? (array) $value : $value;
}
return is_array($value) ? reset($value) : $value;
};
$innerBuilder->addModelTransformer(new CallbackTransformer($transform, $transform));
$form->add($innerBuilder->getForm());
} else {
$attr = [
'class' => 'form-control',
'data-toggle' => $fieldType,
'data-onload-callback' => 'updateLeadFieldValueOptions',
];
if (!$supportsValue) {
$attr['disabled'] = 'disabled';
}
$form->add(
'value',
TextType::class,
[
'label' => 'mautic.form.field.form.value',
'label_attr' => ['class' => 'control-label'],
'attr' => $attr,
'constraints' => ($supportsValue) ? [
new NotBlank(
['message' => 'mautic.core.value.required']
),
] : [],
]
);
}
$form->add(
'operator',
ChoiceType::class,
[
'choices' => $this->leadModel->getOperatorsForFieldType(null == $fieldType ? 'default' : $fieldType, ['date']),
'label' => 'mautic.lead.lead.submitaction.operator',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'onchange' => 'Mautic.updateLeadFieldValues(this)',
],
]
);
};
// Register the function above as EventListener on PreSet and PreBind
$builder->addEventListener(FormEvents::PRE_SET_DATA, $func);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $func);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_field_value';
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\UserBundle\Form\Type\UserListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadOwnerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'owner',
UserListType::class,
[
'label' => 'mautic.lead.lead.field.owner',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
'multiple' => true,
]
);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_owner';
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadSegmentsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'segments',
LeadListType::class,
[
'global_only' => true,
'label' => 'mautic.lead.lead.lists',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'required' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_segments';
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\StageBundle\Form\Type\StageListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadStagesType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'stages',
StageListType::class,
[
'label' => 'mautic.lead.lead.field.stage',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'required' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_stages';
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<mixed>
*/
class CampaignEventLeadTagsType extends AbstractType
{
public function __construct(
private TranslatorInterface $translator,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'tags',
TagType::class,
[
'add_transformer' => true,
'by_reference' => false,
'attr' => [
'data-placeholder' => $this->translator->trans('mautic.lead.tags.select_or_create'),
'data-no-results-text' => $this->translator->trans('mautic.lead.tags.enter_to_create'),
'data-allow-add' => 'true',
'onchange' => 'Mautic.createLeadTag(this)',
],
]
);
}
public function getBlockPrefix(): string
{
return 'campaignevent_lead_tags';
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Provider\TypeOperatorProviderInterface;
use Mautic\LeadBundle\Segment\OperatorOptions;
use Mautic\PointBundle\Form\Type\GroupListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<CampaignEventPointType>
*/
class CampaignEventPointType extends AbstractType
{
public function __construct(
private TypeOperatorProviderInterface $typeOperatorProvider,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'operator',
ChoiceType::class,
[
'label' => 'mautic.lead.campaign.event.point_operator',
'multiple' => false,
'choices' => $this->typeOperatorProvider->getOperatorsIncluding([
OperatorOptions::EQUAL_TO,
OperatorOptions::NOT_EQUAL_TO,
OperatorOptions::GREATER_THAN,
OperatorOptions::LESS_THAN,
OperatorOptions::GREATER_THAN_OR_EQUAL,
OperatorOptions::LESS_THAN_OR_EQUAL,
]),
'required' => true,
'label_attr' => ['class' => 'control-label'],
]
);
$builder->add(
'score',
NumberType::class,
[
'label' => 'mautic.lead.campaign.event.point_score',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'scale' => 0,
'required' => true,
]
);
$builder->add('group', GroupListType::class, [
'label' => 'mautic.lead.campaign.event.point_group',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.campaign.event.point_group.help',
],
'required' => false,
'by_reference' => false,
'return_entity' => false,
]);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\UserBundle\Model\UserModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class ChangeOwnerType extends AbstractType
{
public function __construct(
private UserModel $userModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'owner',
ChoiceType::class,
[
'label' => 'mautic.lead.batch.add_to',
'multiple' => false,
'choices' => $this->userModel->getOwnerListChoices(),
'required' => true,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'buttons',
FormButtonsType::class
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\NotEqualTo;
/**
* @extends AbstractType<mixed>
*/
class CompanyChangeScoreActionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'score',
NumberType::class,
[
'label' => 'mautic.lead.lead.events.changecompanyscore',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'scale' => 0,
'data' => $options['data']['score'] ?? 0,
'constraints' => [
new NotEqualTo(
[
'value' => 0,
'message' => 'mautic.core.value.required',
]
),
],
]
);
}
public function getBlockPrefix(): string
{
return 'scorecontactscompanies_action';
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Mautic\LeadBundle\Entity\CompanyRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class CompanyListType extends AbstractType
{
public const DEFAULT_LIMIT = 100;
public function __construct(
private CompanyRepository $companyRepository,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'label' => 'mautic.lead.lead.companies',
'entity_label_column' => 'companyname',
'modal_route' => 'mautic_company_action',
'modal_header' => 'mautic.company.new.company',
'model' => 'lead.company',
'ajax_lookup_action' => 'lead:getLookupChoiceList',
'model_lookup_method' => 'getLookupResults',
'lookup_arguments' => fn (Options $options): array => [
'type' => 'lead.company',
'limit' => self::DEFAULT_LIMIT,
] + ((isset($options['model_lookup_method']) && ('getSimpleLookupResults' === $options['model_lookup_method'])) ? ['exclude' => $options['main_entity']] : []),
'multiple' => true,
'main_entity' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$data = $form->getData();
if ($data) {
$selectedIds = is_array($data) ? $data : [$data];
$existingChoices = array_column($view->vars['choices'], 'value');
$missingIds = array_diff($selectedIds, $existingChoices);
if ($missingIds) {
$missingCompanies = $this->companyRepository->findBy(['id' => $missingIds]);
foreach ($missingCompanies as $company) {
$view->vars['choices'][] = new ChoiceView(
$company->getId(),
(string) $company->getId(),
$company->getName()
);
}
}
}
}
public function getParent(): ?string
{
return EntityLookupType::class;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class CompanyMergeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'company_to_merge',
CompanyListType::class,
[
'multiple' => false,
'label' => 'mautic.company.to.merge.into',
'required' => true,
'modal_route' => false,
'main_entity' => $options['main_entity'],
'model_lookup_method' => $options['model_lookup_method'],
'constraints' => [
new NotBlank(
['message' => 'mautic.company.choosecompany.notblank']
),
],
]
);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.lead.merge',
'save_icon' => 'ri-building-2-line',
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(
['main_entity', 'model_lookup_method']
);
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\LeadBundle\Entity\Company;
use Mautic\ProjectBundle\Form\Type\ProjectType;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Form\Type\UserListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<Company>
*/
class CompanyType extends AbstractType
{
use EntityFieldsBuildFormTrait;
public function __construct(
private EntityManager $em,
protected RouterInterface $router,
protected TranslatorInterface $translator,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$cleaningRules = $this->getFormFields($builder, $options, 'company');
$cleaningRules['companyemail'] = 'email';
$transformer = new IdToEntityModelTransformer($this->em, User::class);
$builder->add(
$builder->create(
'owner',
UserListType::class,
[
'label' => 'mautic.lead.company.field.owner',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
'multiple' => false,
]
)
->addModelTransformer($transformer)
);
$builder->add(
'score',
NumberType::class,
[
'label' => 'mautic.company.score',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'scale' => 0,
'required' => false,
]
);
$builder->add('projects', ProjectType::class);
if (!empty($options['update_select'])) {
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
]
);
$builder->add(
'updateSelect',
HiddenType::class,
[
'data' => $options['update_select'],
'mapped' => false,
]
);
} else {
$builder->add(
'buttons',
FormButtonsType::class
);
}
if (null === $options['data']->getId()) {
$builder->add(
'buttons',
FormButtonsType::class
);
} else {
$builder->add(
'buttons',
FormButtonsType::class,
[
'post_extra_buttons' => [
[
'name' => 'merge',
'label' => 'mautic.lead.merge',
'attr' => [
'class' => 'btn btn-ghost btn-dnd',
'icon' => 'ri-building-2-line',
'data-toggle' => 'ajaxmodal',
'data-target' => '#MauticSharedModal',
'data-header' => $this->translator->trans('mautic.lead.company.header.merge'),
'href' => $this->router->generate(
'mautic_company_action',
[
'objectId' => $options['data']->getId(),
'objectAction' => 'merge',
]
),
],
],
],
]
);
}
$builder->addEventSubscriber(new CleanFormSubscriber($cleaningRules));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Company::class,
'isShortForm' => false,
'update_select' => false,
]
);
$resolver->setRequired(['fields']);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class ConfigCompanyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'company_unique_identifiers_operator',
ChoiceType::class,
[
'choices' => [
'mautic.core.config.contact_unique_identifiers_operator.or' => CompositeExpression::TYPE_OR,
'mautic.core.config.contact_unique_identifiers_operator.and' => CompositeExpression::TYPE_AND,
],
'label' => 'mautic.core.config.unique_identifiers_operator',
'required' => false,
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.core.config.unique_identifiers_operator.tooltip',
],
'placeholder' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'companyconfig';
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class ConfigType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'contact_allow_multiple_companies',
YesNoButtonGroupType::class,
[
'label' => 'mautic.core.config.allow_multiple_companies',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.core.config.allow_multiple_companies.tooltip',
],
'data' => (bool) ($options['data']['contact_allow_multiple_companies'] ?? true),
]
);
$builder->add(
'contact_unique_identifiers_operator',
ChoiceType::class,
[
'choices' => [
'mautic.core.config.contact_unique_identifiers_operator.or' => CompositeExpression::TYPE_OR,
'mautic.core.config.contact_unique_identifiers_operator.and' => CompositeExpression::TYPE_AND,
],
'label' => 'mautic.core.config.unique_identifiers_operator',
'required' => false,
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.core.config.unique_identifiers_operator.tooltip',
],
'placeholder' => false,
]
);
$builder->add(
'background_import_if_more_rows_than',
NumberType::class,
[
'label' => 'mautic.lead.background.import.if.more.rows.than',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.background.import.if.more.rows.than.tooltip',
],
]
);
$builder->add('contact_export_limit', NumberType::class, [
'label' => 'mautic.lead.export.limit.rows',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.export.limit.rows.tooltip',
],
'required' => false,
'data' => $options['data']['contact_export_limit'] ?? 0,
'constraints' => [
new GreaterThanOrEqual([
'value' => 0,
]),
],
]);
$formModifier = static function (FormInterface $form, $currentColumns): void {
$order = [];
$orderColumns = [];
if (!empty($currentColumns)) {
$orderColumns = array_values($currentColumns);
$order = htmlspecialchars(json_encode($orderColumns), ENT_QUOTES, 'UTF-8');
}
$form->add(
'contact_columns',
ContactColumnsType::class,
[
'label' => 'mautic.config.tab.columns',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control multiselect',
'data-sortable' => 'true',
'data-order' => $order,
],
'multiple' => true,
'required' => true,
'expanded' => false,
'constraints' => [
new NotBlank(
['message' => 'mautic.core.value.required']
),
],
'data'=> array_flip($orderColumns),
]
);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier): void {
$data = $event->getData();
$columns = $data['contact_columns'] ?? [];
$formModifier($event->getForm(), $columns);
}
);
// Build the columns selector
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier): void {
$data = $event->getData();
$columns = $data['contact_columns'] ?? [];
$formModifier($event->getForm(), $columns);
}
);
$builder->add(
'contact_export_in_background',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.background.export.csv',
'data' => $options['data']['contact_export_in_background'] ?? false,
'attr' => [
'tooltip' => 'mautic.lead.background.export.csv.tooltip',
],
]
);
}
public function getBlockPrefix(): string
{
return 'leadconfig';
}
}

View File

@@ -0,0 +1,204 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\LeadBundle\Entity\FrequencyRule;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class ContactChannelsType extends AbstractType
{
public function __construct(
private CoreParametersHelper $coreParametersHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$showContactFrequency = $this->coreParametersHelper->get('show_contact_frequency');
$showContactPauseDates = $this->coreParametersHelper->get('show_contact_pause_dates');
$showContactPreferredChannels = $this->coreParametersHelper->get('show_contact_preferred_channels');
$builder->add(
'subscribed_channels',
ChoiceType::class,
[
'choices' => $options['channels'],
'expanded' => true,
'label_attr' => ['class' => 'control-label'],
'attr' => ['onClick' => 'Mautic.togglePreferredChannel(this.value);'],
'multiple' => true,
'label' => false,
'required' => false,
]
);
if (!$options['public_view'] || $showContactPreferredChannels) {
$builder->add(
'preferred_channel',
ChoiceType::class,
[
'choices' => $options['channels'],
'expanded' => false,
'multiple' => false,
'label' => 'mautic.lead.list.frequency.preferred.channel',
'label_attr' => ['class' => 'control-label'],
'placeholder' => false,
'required' => false,
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.list.frequency.preferred.channel',
],
]
);
}
if (!$options['public_view'] || $showContactFrequency || $showContactPauseDates) {
foreach ($options['channels'] as $channel) {
$attr = (isset($options['data']['subscribed_channels']) && !in_array($channel, $options['data']['subscribed_channels']))
? ['disabled' => 'disabled'] : [];
$builder->add(
'frequency_number_'.$channel,
IntegerType::class,
[
'label' => 'mautic.lead.list.frequency.number',
'label_attr' => ['class' => 'text-secondary fw-n label1'],
'attr' => array_merge(
$attr,
[
'class' => 'frequency form-control',
]
),
'required' => false,
]
);
$builder->add(
'frequency_time_'.$channel,
ChoiceType::class,
[
'choices' => [
'mautic.core.time.days' => FrequencyRule::TIME_DAY,
'mautic.core.time.weeks' => FrequencyRule::TIME_WEEK,
'mautic.core.time.months' => FrequencyRule::TIME_MONTH,
],
'label' => 'mautic.lead.list.frequency.times',
'label_attr' => ['class' => 'text-secondary fw-n frequency-label label2'],
'multiple' => false,
'required' => false,
'attr' => array_merge(
$attr,
[
'class' => 'form-control',
]
),
]
);
if (false == $options['public_view']) {
$attributes = array_merge(
$attr,
[
'data-toggle' => 'date',
'class' => 'frequency-date form-control',
]
);
} else {
$attributes = array_merge(
$attr,
[
'class' => 'form-control',
]
);
}
if (!$options['public_view'] || $showContactPauseDates) {
$builder->add(
'contact_pause_start_date_'.$channel,
DateType::class,
$this->configureDateTypeOptions([
'widget' => 'single_text',
'label' => false,
'label_attr' => ['class' => 'text-secondary fw-n label3'],
'attr' => $attributes,
'required' => false,
], $options['public_view'])
);
$builder->add(
'contact_pause_end_date_'.$channel,
DateType::class,
$this->configureDateTypeOptions([
'widget' => 'single_text',
'label' => 'mautic.lead.frequency.contact.end.date',
'label_attr' => ['class' => 'frequency-label text-secondary fw-n label4'],
'attr' => $attributes,
'required' => false,
], $options['public_view'])
);
}
}
}
if (isset($options['save_button']) && true === $options['save_button']) {
$builder->add(
'ids',
HiddenType::class
);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
}
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['channels']);
$resolver->setDefaults(
[
'public_view' => false,
'save_button' => false,
]
);
}
/**
* @param array<string,string|bool|mixed[]> $options
*
* @return array<string,string|bool|mixed[]>
*/
private function configureDateTypeOptions(array $options, bool $useHtml5): array
{
$options['html5'] = $useHtml5;
if (!$useHtml5) {
$options['format'] = 'yyyy-MM-dd';
}
return $options;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Services\ContactColumnsDictionary;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class ContactColumnsType extends AbstractType
{
public function __construct(
private ContactColumnsDictionary $columnsDictionary,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'choices' => array_flip($this->columnsDictionary->getFields()),
'label' => false,
'label_attr' => ['class' => 'control-label'],
'required' => false,
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'form-control',
],
]
);
}
public function getParent(): ?string
{
return ChoiceType::class;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class ContactFrequencyType extends AbstractType
{
public function __construct(
protected CoreParametersHelper $coreParametersHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$showContactCategories = $this->coreParametersHelper->get('show_contact_categories');
$showContactSegments = $this->coreParametersHelper->get('show_contact_segments');
if (!empty($options['channels'])) {
$builder->add(
'lead_channels',
ContactChannelsType::class,
[
'label' => false,
'channels' => $options['channels'],
'data' => $options['data']['lead_channels'],
'public_view' => $options['public_view'],
]
);
}
if (!$options['public_view']) {
$builder->add(
'lead_lists',
LeadListType::class,
[
'label' => 'mautic.lead.form.list',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'expanded' => $options['public_view'],
'required' => false,
]
);
} elseif ($showContactSegments) {
$builder->add(
'lead_lists',
LeadListType::class,
[
'preference_center_only' => $options['preference_center_only'],
'label' => 'mautic.lead.form.list',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'expanded' => true,
'required' => false,
]
);
}
if (!$options['public_view'] || $showContactCategories) {
$builder->add(
'global_categories',
LeadCategoryType::class,
[
'label' => 'mautic.lead.form.categories',
'label_attr' => ['class' => 'control-label'],
'multiple' => true,
'expanded' => $options['public_view'],
'required' => false,
]
);
}
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['channels']);
$resolver->setDefaults(
[
'public_view' => false,
'preference_center_only' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'lead_contact_frequency_rules';
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\PointBundle\Entity\Group;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class ContactGroupPointsType extends AbstractType
{
private const SCORE_FIELD_PREFIX = 'score_group_';
public function __construct(
private TranslatorInterface $translator,
) {
}
public static function getFieldKey(int $groupId): string
{
return self::SCORE_FIELD_PREFIX.$groupId;
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$pointGroups = $options['point_groups'] ?? [];
/** @var Group $group */
foreach ($pointGroups as $group) {
$key = self::getFieldKey($group->getId());
$builder->add(
$key,
IntegerType::class,
[
'label' => $group->getName(),
'attr' => [
'class' => 'form-control',
'placeholder' => $this->translator->trans('mautic.point.form.score_not_set'),
],
'label_attr' => ['class' => 'control-label'],
'required' => false,
]
);
}
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['point_groups']);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class DashboardLeadsInTimeWidgetType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'flag',
ChoiceType::class,
[
'label' => 'mautic.lead.list.filter',
'choices' => [
'mautic.lead.show.all' => '',
'mautic.lead.show.identified' => 'identified',
'mautic.lead.show.anonymous' => 'anonymous',
'mautic.lead.show.identified.vs.anonymous' => 'identifiedVsAnonymous',
'mautic.lead.show.top' => 'top',
'mautic.lead.show.top.leads.identified.vs.anonymous' => 'topIdentifiedVsAnonymous',
],
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'empty_data' => '',
'required' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'lead_dashboard_leads_in_time_widget';
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Model\ListModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<mixed>
*/
class DashboardLeadsLifetimeWidgetType extends AbstractType
{
public function __construct(
private ListModel $segmentModel,
private TranslatorInterface $translator,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$lists = $this->segmentModel->getUserLists();
$segments = [];
$segments[$this->translator->trans('mautic.lead.all.leads')] = 0;
foreach ($lists as $list) {
$segments[$list['name']] = $list['id'];
}
$builder->add('flag', ChoiceType::class, [
'label' => 'mautic.lead.list.filter',
'multiple' => true,
'choices' => $segments,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'lead_dashboard_leads_lifetime_widget';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Model\ListModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class DashboardSegmentsBuildTime extends AbstractType
{
public function __construct(
private ListModel $segmentModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'order',
ChoiceType::class,
[
'label' => 'mautic.core.order',
'choices' => [
'mautic.widget.segments.build.time.shortest' => 'ASC',
'mautic.widget.segments.build.time.longest' => 'DESC',
],
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'empty_data' => '',
'required' => false,
]
);
$lists = $this->segmentModel->getUserLists();
$segments = [];
foreach ($lists as $list) {
$segments[$list['name']] = $list['id'];
}
$builder->add('segments', ChoiceType::class, [
'label' => 'mautic.lead.list.filter',
'multiple' => true,
'choices' => $segments,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
]
);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\LeadBundle\Entity\LeadDevice;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<LeadDevice>
*/
class DeviceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('device', TextType::class);
$builder->add('deviceOsName', TextType::class);
$builder->add('deviceOsShortName', TextType::class);
$builder->add('deviceOsVersion', TextType::class);
$builder->add('deviceOsPlatform', TextType::class);
$builder->add('deviceModel', TextType::class);
$builder->add('deviceBrand', TextType::class);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => LeadDevice::class,
]
);
}
public function getBlockPrefix(): string
{
return 'leaddevice';
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class DncType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'reason',
TextareaType::class,
[
'label' => 'mautic.lead.batch.dnc_reason',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'ids',
HiddenType::class
);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function getBlockPrefix(): string
{
return 'lead_batch_dnc';
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\EmailBundle\Form\Type\EmailListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class EmailType extends AbstractType
{
public const REPLY_TO_ADDRESS = 'replyToAddress';
public function __construct(
private UserHelper $userHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CleanFormSubscriber(['body' => 'raw']));
$builder->add(
'subject',
TextType::class,
[
'label' => 'mautic.email.subject',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'empty_data' => '',
]
);
$user = $this->userHelper->getUser();
$default = (empty($options['data']['fromname'])) ? $user->getFirstName().' '.$user->getLastName() : $options['data']['fromname'];
$builder->add(
'fromname',
TextType::class,
[
'label' => 'mautic.lead.email.from_name',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'preaddon' => 'ri-user-6-fill',
],
'required' => false,
'data' => $default,
]
);
$default = (empty($options['data']['from'])) ? $user->getEmail() : $options['data']['from'];
$builder->add(
'from',
TextType::class,
[
'label' => 'mautic.lead.email.from_email',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'preaddon' => 'ri-mail-line',
],
'required' => false,
'data' => $default,
'constraints' => [
new NotBlank([
'message' => 'mautic.core.email.required',
]),
new Email([
'message' => 'mautic.core.email.required',
]),
],
]
);
$builder->add(
self::REPLY_TO_ADDRESS,
TextType::class,
[
'label' => 'mautic.email.reply_to_email',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'preaddon' => 'ri-mail-line',
'tooltip' => 'mautic.email.reply_to_email.tooltip',
],
'required' => false,
]
);
$builder->add(
'body',
TextareaType::class,
[
'label' => 'mautic.email.form.body',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control editor editor-basic-fullpage editor-builder-tokens editor-email',
'data-token-callback' => 'email:getBuilderTokens',
'data-token-activator' => '{',
'allow-full-html' => true,
],
]
);
$builder->add('list', HiddenType::class);
$builder->add(
'templates',
EmailListType::class,
[
'label' => 'mautic.lead.email.template',
'label_attr' => ['class' => 'control-label'],
'required' => false,
'attr' => [
'class' => 'form-control',
'onchange' => 'Mautic.getLeadEmailContent(this)',
],
'multiple' => false,
]
);
$builder->add('buttons', FormButtonsType::class, [
'apply_text' => false,
'save_text' => 'mautic.email.send',
'save_class' => 'btn btn-primary',
'save_icon' => 'ri-send-plane-line',
'cancel_icon' => 'ri-close-line',
]);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function getBlockPrefix(): string
{
return 'lead_quickemail';
}
}

View File

@@ -0,0 +1,298 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\BooleanType;
use Mautic\CoreBundle\Form\Type\CountryType;
use Mautic\CoreBundle\Form\Type\LocaleType;
use Mautic\CoreBundle\Form\Type\LookupType;
use Mautic\CoreBundle\Form\Type\MultiselectType;
use Mautic\CoreBundle\Form\Type\RegionType;
use Mautic\CoreBundle\Form\Type\SelectType;
use Mautic\CoreBundle\Form\Type\TimezoneType;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\LeadBundle\Exception\FieldNotFoundException;
use Mautic\LeadBundle\Form\FieldAliasToFqcnMap;
use Mautic\LeadBundle\Form\Validator\Constraints\EmailAddress;
use Mautic\LeadBundle\Helper\FormFieldHelper;
use Mautic\LeadBundle\Validator\Constraints\Length;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\Constraints\NotBlank;
trait EntityFieldsBuildFormTrait
{
/**
* @return array<string, 'html'|'raw'>
*/
private function getFormFields(FormBuilderInterface $builder, array $options, $object = 'lead'): array
{
$cleaningRules = [];
$fieldValues = [];
$isObject = false;
if (!empty($options['data'])) {
$isObject = is_object($options['data']);
$fieldValues = ($isObject) ? $options['data']->getFields() : $options['data'];
}
$mapped = !$isObject;
foreach ($options['fields'] as $field) {
if (false === $field['isPublished'] || $field['object'] !== $object) {
continue;
}
$attr = ['class' => 'form-control'];
$properties = $field['properties'];
$type = $field['type'];
$required = ($isObject) ? $field['isRequired'] : false;
$alias = $field['alias'];
$group = $field['group'];
try {
$type = FieldAliasToFqcnMap::getFqcn($type);
} catch (FieldNotFoundException) {
}
if ($field['isUniqueIdentifer']) {
$attr['data-unique-identifier'] = $field['alias'];
}
if ($isObject) {
$value = $fieldValues[$group][$alias]['value'] ?? $field['defaultValue'];
} else {
$value = $fieldValues[$alias] ?? '';
}
$constraints = [];
if ($required && empty($options['ignore_required_constraints'])) {
$constraints[] = new NotBlank(
['message' => 'mautic.lead.customfield.notblank']
);
} elseif (!empty($options['ignore_required_constraints'])) {
$required = false;
$field['isRequired'] = false;
}
if ($field['charLengthLimit'] > 0) {
$constraints[] = new Length(['max' => $field['charLengthLimit']]);
}
switch ($type) {
case NumberType::class:
if (empty($properties['scale'])) {
$properties['scale'] = null;
} // ensure default locale is used
else {
$properties['scale'] = (int) $properties['scale'];
}
if ('' === $value) {
// Prevent transform errors
$value = null;
}
$builder->add(
$alias,
$type,
[
'required' => $required,
'label' => $field['label'],
'label_attr' => ['class' => 'control-label'],
'attr' => $attr,
'data' => (null !== $value) ? (float) $value : $value,
'mapped' => $mapped,
'constraints' => $constraints,
'scale' => $properties['scale'],
'rounding_mode' => isset($properties['roundmode']) ? (int) $properties['roundmode'] : 0,
]
);
break;
case DateType::class:
case DateTimeType::class:
case TimeType::class:
$opts = [
'required' => $required,
'label' => $field['label'],
'label_attr' => ['class' => 'control-label'],
'attr' => $attr,
'mapped' => $mapped,
'constraints' => $constraints,
];
if (!empty($options['ignore_date_type'])) {
$type = TextType::class;
} else {
$opts['html5'] = false;
$opts['input'] = 'string';
$opts['widget'] = 'single_text';
$opts['html5'] = false;
if ($value) {
try {
$dtHelper = new DateTimeHelper($value, null, 'local');
} catch (\Exception) {
// Rather return empty value than break the page
$value = null;
}
}
if (DateTimeType::class === $type) {
$opts['attr']['data-toggle'] = 'datetime';
$opts['model_timezone'] = 'UTC';
$opts['view_timezone'] = date_default_timezone_get();
$opts['format'] = 'yyyy-MM-dd HH:mm:ss';
$opts['with_seconds'] = true;
$opts['data'] = (!empty($value)) ? $dtHelper->toLocalString('Y-m-d H:i:s') : null;
} elseif (DateType::class === $type) {
$opts['attr']['data-toggle'] = 'date';
$opts['data'] = (!empty($value)) ? $dtHelper->toLocalString('Y-m-d') : null;
} else {
$opts['attr']['data-toggle'] = 'time';
// $opts['with_seconds'] = true; // @todo figure out why this cause the contact form to fail.
$opts['data'] = (!empty($value)) ? $dtHelper->toLocalString('H:i:s') : null;
}
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($alias, $type): void {
$data = $event->getData();
if (!empty($data[$alias])) {
if (false === ($timestamp = strtotime($data[$alias]))) {
$timestamp = null;
}
if ($timestamp) {
$dtHelper = new DateTimeHelper(date('Y-m-d H:i:s', $timestamp), null, 'local');
switch ($type) {
case DateTimeType::class:
$data[$alias] = $dtHelper->toLocalString('Y-m-d H:i:s');
break;
case DateType::class:
$data[$alias] = $dtHelper->toLocalString('Y-m-d');
break;
case TimeType::class:
$data[$alias] = $dtHelper->toLocalString('H:i:s');
break;
}
}
}
$event->setData($data);
}
);
}
$builder->add($alias, $type, $opts);
break;
case SelectType::class:
case MultiselectType::class:
case BooleanType::class:
if (MultiselectType::class === $type) {
$constraints[] = new Length(['max' => 65535]);
}
$typeProperties = [
'required' => $required,
'label' => $field['label'],
'attr' => $attr,
'mapped' => $mapped,
'constraints' => $constraints,
];
$emptyValue = '';
if (array_key_exists('use_nullable_yes_no_type', $options) && true === $options['use_nullable_yes_no_type'] && BooleanType::class === $type) {
$type = NullableYesNoButtonGroupType::class;
$emptyValue = 'mautic.core.form.no_change';
} elseif (in_array($type, [SelectType::class, MultiselectType::class]) && !empty($properties['list'])) {
$typeProperties['choices'] = array_flip(FormFieldHelper::parseList($properties['list']));
$cleaningRules[$field['alias']] = 'raw';
}
if (BooleanType::class === $type && !empty($properties['yes']) && !empty($properties['no'])) {
$typeProperties['yes_label'] = $properties['yes'];
$typeProperties['no_label'] = $properties['no'];
$emptyValue = ' x ';
if ('' !== $value && null !== $value) {
$value = (int) $value;
}
}
$typeProperties['data'] = MultiselectType::class === $type ? FormFieldHelper::parseList($value) : $value;
$typeProperties['placeholder'] = $emptyValue;
$builder->add(
$alias,
$type,
$typeProperties
);
break;
case CountryType::class:
case RegionType::class:
case TimezoneType::class:
case LocaleType::class:
$builder->add(
$alias,
$type,
[
'required' => $required,
'label' => $field['label'],
'data' => $value,
'attr' => [
'class' => 'form-control',
'data-placeholder' => $field['label'],
],
'mapped' => $mapped,
'constraints' => $constraints,
]
);
break;
default:
$attr['data-encoding'] = 'raw';
switch ($type) {
case LookupType::class:
$attr['data-target'] = $alias;
$constraints[] = new Length(['max' => 191]);
if (!empty($properties['list'])) {
$attr['data-options'] = FormFieldHelper::formatList(FormFieldHelper::FORMAT_BAR, array_keys(FormFieldHelper::parseList($properties['list'])));
}
break;
case EmailType::class:
// Enforce a valid email
$attr['data-encoding'] = 'email';
$constraints[] = new EmailAddress();
break;
case TextType::class:
$constraints[] = new Length(['max' => 191]);
break;
case MultiselectType::class:
$constraints[] = new Length(['max' => 65535]);
break;
case HtmlType::class:
$cleaningRules[$field['alias']] = 'html';
break;
}
$builder->add(
$alias,
$type,
[
'required' => $field['isRequired'],
'label' => $field['label'],
'label_attr' => ['class' => 'control-label'],
'attr' => $attr,
'data' => $value,
'mapped' => $mapped,
'constraints' => $constraints,
]
);
break;
}
}
return $cleaningRules;
}
}

View File

@@ -0,0 +1,744 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\SortableListType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Entity\LeadFieldRepository;
use Mautic\LeadBundle\Field\Helper\IndexHelper;
use Mautic\LeadBundle\Field\IdentifierFields;
use Mautic\LeadBundle\Field\SchemaDefinition;
use Mautic\LeadBundle\Form\DataTransformer\FieldToOrderTransformer;
use Mautic\LeadBundle\Helper\FormFieldHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\IsFalse;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @extends AbstractType<LeadField>
*/
class FieldType extends AbstractType
{
/**
* @var string[]
*/
private static array $fieldsWithNoLengthLimit = [
'textarea',
'html',
];
public function __construct(
private EntityManagerInterface $em,
private Translator $translator,
private IdentifierFields $identifierFields,
private IndexHelper $indexHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new FormExitSubscriber('lead.field', $options));
$builder->add(
'label',
TextType::class,
[
'label' => 'mautic.lead.field.label',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control', 'length' => 191],
]
);
$disabled = (!empty($options['data'])) ? $options['data']->isFixed() : false;
$builder->add(
'group',
ChoiceType::class,
[
'choices' => [
'mautic.lead.field.group.core' => 'core',
'mautic.lead.field.group.social' => 'social',
'mautic.lead.field.group.personal' => 'personal',
'mautic.lead.field.group.professional' => 'professional',
],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.field.form.group.help',
'onchange' => 'Mautic.updateLeadFieldOrderChoiceList();',
],
'expanded' => false,
'multiple' => false,
'label' => 'mautic.lead.field.group',
'placeholder' => false,
'required' => false,
'disabled' => $disabled,
]
);
$new = $options['data']->getId() ? false : true;
$type = $options['data']->getType();
$isIndex = $options['data']->isIsIndex();
$default = (empty($type)) ? 'text' : $type;
$fieldHelper = new FormFieldHelper();
$fieldHelper->setTranslator($this->translator);
$builder->add(
'type',
ChoiceType::class,
[
'choices' => $fieldHelper->getChoiceList(),
'expanded' => false,
'multiple' => false,
'label' => 'mautic.lead.field.type',
'placeholder' => false,
'disabled' => ($disabled || !$new),
'attr' => [
'class' => 'form-control',
'onchange' => 'Mautic.updateLeadFieldProperties(this.value);',
],
'data' => $default,
'required' => false,
]
);
$builder->add(
'properties_select_template',
SortableListType::class,
[
'mapped' => false,
'label' => 'mautic.lead.field.form.properties.select',
'option_required' => false,
'with_labels' => true,
]
);
$builder->add(
'properties_lookup_template',
SortableListType::class,
[
'mapped' => false,
'label' => 'mautic.lead.field.form.properties.select',
'option_required' => false,
'with_labels' => false,
]
);
$listChoices = [
'country' => FormFieldHelper::getCountryChoices(),
'region' => FormFieldHelper::getRegionChoices(),
'timezone' => FormFieldHelper::getTimezonesChoices(),
'locale' => FormFieldHelper::getLocaleChoices(),
'select' => [],
];
foreach ($listChoices as $listType => $choices) {
$builder->add(
'default_template_'.$listType,
ChoiceType::class,
[
'choices' => $choices,
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control not-chosen'],
'required' => false,
'mapped' => false,
]
);
}
$builder->add(
'default_template_text',
TextType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'mapped' => false,
]
);
$builder->add(
'default_template_textarea',
TextareaType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'mapped' => false,
]
);
$builder->add(
'default_template_boolean',
YesNoButtonGroupType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'mapped' => false,
'data' => 0,
]
);
$builder->add(
'properties',
CollectionType::class,
[
'required' => false,
'allow_add' => true,
'error_bubbling' => false,
]
);
$disableDefaultValue = (!$new && in_array($options['data']->getAlias(), $this->identifierFields->getFieldList($options['data']->getObject())));
$builder->add(
'defaultValue',
TextType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.field.help.defaultvalue',
],
'required' => false,
'disabled' => $disableDefaultValue,
'constraints' => [
new Assert\Callback([$this, 'validateDefaultValue']),
],
]
);
/**
* @see FormEvents::PRE_SET_DATA
* Used as as form modifier before trying to set data
*/
$formModifier = function (FormEvent $event) use ($listChoices, $type, $options, $disableDefaultValue): array {
$cleaningRules = [];
$form = $event->getForm();
$data = $event->getData();
$type = (is_array($data)) ? ($data['type'] ?? $type) : $data->getType();
$constraints = [];
switch ($type) {
case 'select':
case 'lookup':
$constraints = new Assert\Callback([$this, 'validateDefaultValue']);
// no break
case 'multiselect':
$cleaningRules['defaultValue'] = 'raw';
if (is_array($data)) {
$properties = $data['properties'] ?? [];
} else {
$properties = $data->getProperties();
}
$propertiesList['list'] = isset($properties['list']) && 'lookup' === $type ? array_flip(array_filter($properties['list'])) : $properties['list'];
$form->add(
'properties',
SortableListType::class,
[
'required' => false,
'label' => 'mautic.lead.field.form.properties.select',
'data' => $propertiesList,
'with_labels' => ('lookup' !== $type),
'option_constraint' => [],
]
);
$list = isset($properties['list']) ? FormFieldHelper::parseList($properties['list']) : [];
$form->add(
'defaultValue',
ChoiceType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label is-chosen'],
'attr' => ['class' => 'form-control'],
'required' => false,
'choices' => array_flip($list),
'multiple' => 'multiselect' === $type,
'data' => 'multiselect' === $type && is_string($options['data']->getDefaultValue()) ? explode('|', $options['data']->getDefaultValue()) : $options['data']->getDefaultValue(),
'disabled' => $disableDefaultValue,
'constraints' => $constraints,
]
);
break;
case 'country':
case 'locale':
case 'timezone':
case 'region':
$form->add(
'defaultValue',
ChoiceType::class,
[
'choices' => $listChoices[$type],
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'disabled' => $disableDefaultValue,
]
);
break;
case 'boolean':
if (is_array($data)) {
$value = $data['defaultValue'] ?? false;
$yesLabel = !empty($data['properties']['yes']) ? $data['properties']['yes'] : 'mautic.core.form.yes';
$noLabel = !empty($data['properties']['no']) ? $data['properties']['no'] : 'mautic.core.form.no';
} else {
$value = $data->getDefaultValue();
$props = $data->getProperties();
$yesLabel = !empty($props['yes']) ? $props['yes'] : 'mautic.core.form.yes';
$noLabel = !empty($props['no']) ? $props['no'] : 'mautic.core.form.no';
}
if ('' !== $value && null !== $value) {
$value = (int) $value;
}
$form->add(
'defaultValue',
YesNoButtonGroupType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'data' => $value,
'no_label' => $noLabel,
'yes_label' => $yesLabel,
'placeholder' => ' x ',
]
);
break;
case 'datetime':
case 'date':
case 'time':
$constraints = [];
switch ($type) {
case 'datetime':
$constraints = [
new Assert\Callback(
function ($object, ExecutionContextInterface $context): void {
if (!empty($object) && false === \DateTime::createFromFormat('Y-m-d H:i', $object)) {
$context->buildViolation('mautic.lead.datetime.invalid')->addViolation();
}
}
),
];
break;
case 'date':
$constraints = [
new Assert\Callback(
function ($object, ExecutionContextInterface $context): void {
if (!empty($object)) {
$validator = $context->getValidator();
$violations = $validator->validate($object, new Assert\Date());
if (count($violations) > 0) {
$context->buildViolation('mautic.lead.date.invalid')->addViolation();
}
}
}
),
];
break;
case 'time':
$constraints = [
new Assert\Callback(
function ($object, ExecutionContextInterface $context): void {
if (!empty($object)) {
$validator = $context->getValidator();
$violations = $validator->validate(
$object,
new Assert\Regex(['pattern' => '/(2[0-3]|[01][0-9]):([0-5][0-9])/'])
);
if (count($violations) > 0) {
$context->buildViolation('mautic.lead.time.invalid')->addViolation();
}
}
}
),
];
break;
}
$form->add(
'defaultValue',
TextType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'data-toggle' => $type,
],
'required' => false,
'constraints' => $constraints,
]
);
break;
case 'tel':
case 'url':
case 'email':
$constraints = new Assert\Callback([$this, 'validateDefaultValue']);
// no break
case 'number':
$form->add(
'defaultValue',
TextType::class,
[
'label' => 'mautic.core.defaultvalue',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'type' => $type,
],
'required' => false,
'disabled' => $disableDefaultValue,
'constraints' => $constraints,
]
);
break;
}
if (in_array($type, LeadField::TYPES_SUPPORTING_LENGTH)) {
$this->addLengthValidationField($form);
}
return $cleaningRules;
};
$setupOrderField = function (FormInterface $form, ?string $object = null, ?string $group = null) use ($builder, $disabled): void {
/** @var LeadFieldRepository $leadFieldRepository */
$leadFieldRepository = $this->em->getRepository(LeadField::class);
$options = [
'label' => 'mautic.core.order.field',
'class' => LeadField::class,
'choice_label' => 'label',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => $disabled ? 'mautic.core.order.field.tooltip.disabled' : 'mautic.core.order.field.tooltip',
],
'required' => false,
'auto_initialize' => false,
'disabled' => $disabled,
];
// There's no need to filter list during FormEvents::PRE_SUBMIT.
if ($object && $group) {
$options['query_builder'] = fn (EntityRepository $er) => $er->createQueryBuilder('f')
->orderBy('f.order', Order::Ascending->value)
->where('f.object = :object')
->setParameter('object', $object)
->andWhere('f.group = :group')
->setParameter('group', $group)
->andWhere('f.isFixed = FALSE');
}
// get order list
$transformer = new FieldToOrderTransformer($leadFieldRepository);
$form->add(
$builder->create(
'order',
EntityType::class,
$options,
)->addModelTransformer($transformer)->getForm()
);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier, $setupOrderField): void {
$formModifier($event);
/** @var LeadField $field */
$field = $event->getData();
$setupOrderField($event->getForm(), $field->getObject(), $field->getGroup());
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier, $disableDefaultValue, $setupOrderField): void {
$data = $event->getData();
$cleaningRules = $formModifier($event);
$masks = !empty($cleaningRules) ? $cleaningRules : 'clean';
// clean the data
$data = InputHelper::_($data, $masks);
if ((isset($data['group']) && 'social' === $data['group']) || !empty($data['isUniqueIdentifer']) || $disableDefaultValue) {
// Don't allow a default for social or unique identifiers
$data['defaultValue'] = null;
}
if (isset($data['type']) && !in_array($data['type'], LeadField::TYPES_SUPPORTING_LENGTH)) {
$data['charLengthLimit'] = null;
}
$event->setData($data);
$setupOrderField($event->getForm());
}
);
$builder->add(
'alias',
TextType::class,
[
'label' => 'mautic.core.alias',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'length' => 25,
'tooltip' => 'mautic.lead.field.help.alias',
],
'required' => false,
'disabled' => ($disabled || !$new),
]
);
$attr = [];
if ($options['data']->getColumnIsNotCreated()) {
$attr = [
'tooltip' => 'mautic.lead.field.being_created_in_background',
];
}
if ($options['data']->getColumnIsNotRemoved()) {
if (array_key_exists('tooltip', $attr)) {
$attr['tooltip'] = $attr['tooltip'].' mautic.lead.field.being_removed_in_background';
} else {
$attr['tooltip'] = 'mautic.lead.field.being_removed_in_background';
}
}
$builder->add(
'isPublished',
YesNoButtonGroupType::class,
[
'disabled' => $options['data']->disablePublishChange(),
'attr' => $attr,
'data' => ('email' == $options['data']->getAlias()) ? true : $options['data']->getIsPublished(),
'label' => 'mautic.core.form.available',
]
);
$builder->add(
'isRequired',
YesNoButtonGroupType::class,
[
'label' => 'mautic.core.required',
]
);
$builder->add(
'isVisible',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.field.form.isvisible',
]
);
$builder->add(
'isShortVisible',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.field.form.isshortvisible',
'attr' => [
'tooltip' => 'mautic.lead.field.form.isshortvisible.tooltip',
'data-disable-on' => '{"leadfield_object":"company"}',
],
]
);
$builder->add(
'isListable',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.field.form.islistable',
]
);
$constraints = [];
if (false === $options['data']->isIsindex() && false === $this->indexHelper->isNewIndexAllowed()) {
$constraints[] = new IsFalse(['message' => 'mautic.lead.field.form.index_count.error']);
}
$builder->add(
'isIndex',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.field.indexable',
'label_attr' => ['class' => 'control-label'],
'yes_label' => 'mautic.lead.field.indexable.yes',
'no_label' => 'mautic.lead.field.indexable.no',
'attr' => [
'class' => 'form-control',
'tooltip' => $this->translator->trans('mautic.lead.field.form.isIndex.tooltip', ['%indexCount%' => $this->indexHelper->getIndexCount(), '%maxCount%' => $this->indexHelper->getMaxCount()]),
'readonly'=> (false === $isIndex && $this->indexHelper->getIndexCount() >= $this->indexHelper->getMaxCount()),
],
'required' => false,
'constraints' => $constraints,
]
);
$data = $options['data']->isUniqueIdentifier();
$builder->add(
'isUniqueIdentifer',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.field.form.isuniqueidentifer',
'attr' => [
'tooltip' => 'mautic.lead.field.form.isuniqueidentifer.tooltip',
'onchange' => 'Mautic.displayUniqueIdentifierWarning(this);',
'data-disable-on' => '{"leadfield_object":"company"}',
],
'data' => (!empty($data)),
]
);
$builder->add(
'isPubliclyUpdatable',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.field.form.ispubliclyupdatable',
'attr' => [
'tooltip' => 'mautic.lead.field.form.ispubliclyupdatable.tooltip',
],
]
);
$builder->add(
'object',
ChoiceType::class,
[
'choices' => [
'mautic.lead.contact' => 'lead',
'mautic.company.company' => 'company',
],
'expanded' => false,
'multiple' => false,
'label' => 'mautic.lead.field.object',
'placeholder' => false,
'attr' => [
'class' => 'form-control',
'onchange' => 'Mautic.updateLeadFieldOrderChoiceList();',
],
'required' => false,
'disabled' => ($disabled || !$new),
]
);
$builder->add('buttons', FormButtonsType::class);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => LeadField::class,
'validation_groups' => function (FormInterface $form): array {
$data = $form->getData();
\assert($data instanceof LeadField);
$groups = ['Default'];
if ($data->supportsLength()) {
$groups[] = 'indexableFieldWithLimits';
}
return $groups;
},
]
);
}
public function getBlockPrefix(): string
{
return 'leadfield';
}
public static function validateDefaultValue(?string $value, ExecutionContextInterface $context): void
{
if (empty($value)) {
return;
}
/** @var LeadField $field */
$field = $context->getRoot()->getViewData();
if (in_array($field->getType(), self::$fieldsWithNoLengthLimit)) {
return;
}
$limit = $field->getCharLengthLimit();
$defaultValueLength = mb_strlen($value);
if ($defaultValueLength <= $limit) {
return;
}
$translationParameters = [
'%currentLength%' => $defaultValueLength,
'%defaultValueLengthLimit%' => $limit,
];
$context
->buildViolation('mautic.lead.defaultValue.maxlengthexceeded', $translationParameters)
->addViolation();
}
private function addLengthValidationField(FormInterface $form): void
{
$typesWithMaxLength = implode('","', LeadField::TYPES_SUPPORTING_LENGTH);
$form->add(
'charLengthLimit',
NumberType::class,
[
'label' => 'mautic.lead.field.form.maximum.character.length',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'data-show-on' => '{
"leadfield_type":["'.$typesWithMaxLength.'"]
}',
],
'constraints' => [
new Assert\NotBlank(['groups' => 'indexableFieldWithLimits']),
new Assert\Range(['min' => 1, 'max' => SchemaDefinition::MAX_VARCHAR_LENGTH, 'groups' => 'indexableFieldWithLimits']),
],
]
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* This form is filled with the LeadEvents::ADJUST_FILTER_FORM_TYPE_FOR_FIELD subscribers.
*
* @extends AbstractType<mixed>
*/
class FilterPropertiesType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
// This form is loaded via AJAX as part of another form.
// Disable CSRF protection to avoid validation errors with unexpected fileds.
$resolver->setDefaults(['csrf_protection' => false]);
}
}

View File

@@ -0,0 +1,404 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\DBAL\Connection;
use Mautic\LeadBundle\Entity\RegexTrait;
use Mautic\LeadBundle\Helper\FormFieldHelper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
trait FilterTrait
{
use RegexTrait;
/**
* @var Connection
*/
protected $connection;
public function setConnection(Connection $connection): void
{
$this->connection = $connection;
}
/**
* @param string $eventName
*/
public function buildFiltersForm($eventName, FormEvent $event, TranslatorInterface $translator, $currentListId = null): void
{
$data = $event->getData();
$form = $event->getForm();
$options = $form->getConfig()->getOptions();
if (!isset($data['type'])) {
$data['type'] = TextType::class;
$data['field'] = '';
$data['operator'] = null;
}
$fieldType = $data['type'];
$fieldName = $data['field'];
$type = TextType::class;
$attr = ['class' => 'form-control filter-value'];
$displayType = HiddenType::class;
$displayAttr = [];
$operator = $data['operator'] ?? '';
$field = [];
if (isset($options['fields']['behaviors'][$fieldName])) {
$field = $options['fields']['behaviors'][$fieldName];
} elseif (isset($data['object']) && isset($options['fields'][$data['object']][$fieldName])) {
$field = $options['fields'][$data['object']][$fieldName];
}
$customOptions = [];
switch ($fieldType) {
case 'assets':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['assets'];
$customOptions['multiple'] = true;
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
break;
case 'leadlist':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
// Don't show the current list ID in the choices
if (!empty($currentListId)) {
unset($options['lists'][$currentListId]);
}
$customOptions['choices'] = $options['lists'];
$customOptions['multiple'] = in_array($data['operator'], ['in', '!in']);
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
break;
case 'campaign':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['campaign'];
$customOptions['multiple'] = true;
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
break;
case 'lead_email_received':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['emails'];
$customOptions['multiple'] = true;
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
break;
case 'device_type':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['deviceTypes'];
$customOptions['multiple'] = true;
$type = ChoiceType::class;
break;
case 'device_brand':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['deviceBrands'];
$customOptions['multiple'] = true;
$type = ChoiceType::class;
break;
case 'device_os':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['deviceOs'];
$customOptions['multiple'] = true;
$type = ChoiceType::class;
break;
case 'tags':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['tags'];
$customOptions['multiple'] = true;
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
$attr = array_merge(
$attr,
[
'data-placeholder' => $translator->trans('mautic.lead.tags.select_or_create'),
'data-no-results-text' => $translator->trans('mautic.lead.tags.enter_to_create'),
'data-allow-add' => 'true',
'onchange' => 'Mautic.createLeadTag(this)',
]
);
break;
case 'stage':
$customOptions['choices'] = $options['stage'];
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
break;
case 'globalcategory':
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
$customOptions['choices'] = $options['globalcategory'];
$customOptions['multiple'] = true;
$type = ChoiceType::class;
break;
case 'timezone':
case 'country':
case 'region':
case 'locale':
switch ($fieldType) {
case 'timezone':
$choiceKey = 'timezones';
break;
case 'country':
$choiceKey = 'countries';
break;
case 'region':
$choiceKey = 'regions';
break;
case 'locale':
$choiceKey = 'locales';
break;
}
$type = ChoiceType::class;
$customOptions['choices'] = $options[$choiceKey];
$customOptions['choice_translation_domain'] = false;
$customOptions['multiple'] = in_array($operator, ['in', '!in']);
if ($customOptions['multiple']) {
array_unshift($customOptions['choices'], ['' => '']);
if (!isset($data['filter'])) {
$data['filter'] = [];
}
}
break;
case 'time':
case 'date':
case 'datetime':
$attr['data-toggle'] = $fieldType;
break;
case 'lookup_id':
$type = HiddenType::class;
$displayType = TextType::class;
$displayAttr = array_merge(
$displayAttr,
[
'class' => 'form-control',
'data-toggle' => 'field-lookup',
'data-target' => $data['field'],
'data-action' => $field['properties']['data-action'] ?? 'lead:fieldList',
'data-lookup-callback' => $field['properties']['data-lookup-callback'] ?? 'updateLookupListFilter',
'data-callback' => $field['properties']['callback'] ?? 'activateFieldTypeahead',
'placeholder' => $translator->trans(
'mautic.lead.list.form.filtervalue'
),
]
);
if (isset($field['properties']['list'])) {
$displayAttr['data-options'] = $field['properties']['list'];
}
break;
case 'select':
case 'multiselect':
case 'boolean':
$attr = array_merge(
$attr,
[
'placeholder' => $translator->trans('mautic.lead.list.form.filtervalue'),
]
);
if (in_array($operator, ['in', '!in'])) {
$customOptions['multiple'] = true;
if (!isset($data['filter'])) {
$data['filter'] = [];
} elseif (!is_array($data['filter'])) {
$data['filter'] = [$data['filter']];
}
}
$choices = [];
if (!empty($field['properties']['list'])) {
$list = $field['properties']['list'];
$choices = ('boolean' === $fieldType)
? FormFieldHelper::parseBooleanList($list)
: FormFieldHelper::parseList($list);
}
if ('select' === $fieldType) {
// array_unshift cannot be used because numeric values get lost as keys
$choices = array_reverse($choices, true);
$choices[''] = '';
$choices = array_reverse($choices, true);
}
$customOptions['choices'] = $choices;
$customOptions['choice_translation_domain'] = false;
$type = ChoiceType::class;
break;
case 'lookup':
$attr = array_merge(
$attr,
[
'data-toggle' => 'field-lookup',
'data-target' => $data['field'] ?? '',
'data-action' => 'lead:fieldList',
'placeholder' => $translator->trans('mautic.lead.list.form.filtervalue'),
]
);
if (isset($field['properties']['list'])) {
$attr['data-options'] = $field['properties']['list'];
}
break;
}
$customOptions['constraints'] = [];
if (in_array($operator, ['empty', '!empty'])) {
$attr['disabled'] = 'disabled';
} elseif ($operator) {
$customOptions['constraints'][] = new NotBlank(
[
'message' => 'mautic.core.value.required',
]
);
if (in_array($operator, ['regexp', '!regexp']) && $this->connection) {
// Let's add a custom valdiator to test the regex
$customOptions['constraints'][] =
new Callback(
function ($regex, ExecutionContextInterface $context): void {
// Let's test the regex's syntax by making a fake query
try {
$qb = $this->connection->createQueryBuilder();
$qb->select('l.id')
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
->where('l.id REGEXP :regex')
->setParameter('regex', $this->prepareRegex($regex))
->setMaxResults(1);
$qb->executeQuery()->fetchAllAssociative();
} catch (\Exception) {
$context->buildViolation('mautic.core.regex.invalid')->addViolation();
}
}
);
}
}
// @todo implement in UI
if (in_array($operator, ['between', '!between'])) {
$form->add(
'filter',
CollectionType::class,
[
'label' => false,
'entry_type' => $type,
'entry_options' => [
'label' => false,
'attr' => $attr,
],
]
);
} else {
foreach ($customOptions['constraints'] as $i => $constraint) {
if (NotBlank::class === $constraint::class) {
array_splice($customOptions['constraints'], $i, 1);
}
}
if (in_array($data['operator'], ['empty', '!empty'])) {
// @see Symfony\Component\Form\Extension\Core\Type\ChoiceType::configureOptions
$data['filter'] = null;
}
$form->add(
'filter',
$type,
array_merge(
[
'label' => false,
'attr' => $attr,
'data' => $data['filter'] ?? '',
'error_bubbling' => false,
],
$customOptions
)
);
}
$form->add(
'display',
$displayType,
[
'label' => false,
'attr' => $displayAttr,
'data' => $data['display'] ?? '',
'error_bubbling' => false,
]
);
$form->add(
'operator',
ChoiceType::class,
[
'label' => false,
'choices' => $field['operators'] ?? [],
'attr' => [
'class' => 'form-control not-chosen filter-operator',
'onchange' => 'Mautic.convertDwcFilterInput(this)',
],
]
);
if (FormEvents::PRE_SUBMIT === $eventName) {
$event->setData($data);
}
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Model\ListModel;
use Mautic\LeadBundle\Provider\FormAdjustmentsProviderInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class FilterType extends AbstractType
{
public function __construct(
private FormAdjustmentsProviderInterface $formAdjustmentsProvider,
private ListModel $listModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$fieldChoices = $this->listModel->getChoiceFields();
$builder->add(
'glue',
ChoiceType::class,
[
'label' => false,
'choices' => [
'mautic.lead.list.form.glue.and' => 'and',
'mautic.lead.list.form.glue.or' => 'or',
],
'attr' => [
'class' => 'label label-warm-gray not-chosen glue-select',
'onchange' => 'Mautic.updateFilterPositioning(this)',
],
]
);
$formModifier = function (FormEvent $event) use ($fieldChoices): void {
$data = (array) $event->getData();
$form = $event->getForm();
$fieldAlias = $data['field'] ?? null;
$fieldObject = $data['object'] ?? 'behaviors';
// Looking for behaviors for BC reasons as some filters were moved from 'lead' to 'behaviors'.
$field = $fieldChoices[$fieldObject][$fieldAlias] ?? $fieldChoices['behaviors'][$fieldAlias] ?? null;
$operators = $field['operators'] ?? [];
$operator = $data['operator'] ?? null;
if ($operators && !$operator) {
$operator = array_key_first($operators);
}
$form->add(
'operator',
ChoiceType::class,
[
'label' => false,
'choices' => $operators,
'attr' => [
'class' => 'form-control not-chosen',
'onchange' => 'Mautic.convertLeadFilterInput(this)',
],
]
);
$form->add(
'properties',
FilterPropertiesType::class,
[
'label' => false,
]
);
if (null === $field) {
// The field was probably deleted since the segment was created.
// Do not show up the filter based on a deleted field.
return;
}
$filterPropertiesType = $form->get('properties');
$this->setPropertiesFormData($filterPropertiesType, $data);
if ($fieldAlias && $operator) {
$this->formAdjustmentsProvider->adjustForm(
$filterPropertiesType,
$fieldAlias,
$fieldObject,
$operator,
$field
);
}
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, $formModifier);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $formModifier);
$builder->add('field', HiddenType::class);
$builder->add('object', HiddenType::class);
$builder->add('type', HiddenType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'label' => false,
'error_bubbling' => false,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['fields'] = $this->listModel->getChoiceFields();
}
public function getBlockPrefix(): string
{
return 'leadlist_filter';
}
/**
* We have to ensure that the old data[filter] and data[display] will get to the properties form
* to keep BC for segments created before the properties form was added and the fitler and display
* fields were moved there.
*
* @param FormInterface<mixed> $filterPropertiesType
* @param mixed[] $data
*/
private function setPropertiesFormData(FormInterface $filterPropertiesType, array $data): void
{
if (empty($data['properties'])) {
$propertiesData = [
'filter' => $data['filter'] ?? null,
'display' => $data['display'] ?? null,
];
$filterPropertiesType->setData($propertiesData);
} else {
$filterPropertiesType->setData($data['properties'] ?? []);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\PointBundle\Form\Type\GroupListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class FormSubmitActionPointsChangeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'operator',
ChoiceType::class,
[
'label' => 'mautic.lead.lead.submitaction.operator',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'choices' => [
'mautic.lead.lead.submitaction.operator_plus' => 'plus',
'mautic.lead.lead.submitaction.operator_minus' => 'minus',
'mautic.lead.lead.submitaction.operator_times' => 'times',
'mautic.lead.lead.submitaction.operator_divide' => 'divide',
],
]
);
$default = (empty($options['data']['points'])) ? 0 : (int) $options['data']['points'];
$builder->add(
'points',
NumberType::class,
[
'label' => 'mautic.lead.lead.submitaction.points',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'scale' => 0,
'data' => $default,
]
);
$builder->add('group', GroupListType::class, [
'label' => 'mautic.lead.campaign.event.point_group',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.campaign.event.point_group.help',
],
'required' => false,
'by_reference' => false,
'return_entity' => false,
]);
}
public function getBlockPrefix(): string
{
return 'lead_submitaction_pointschange';
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class GlobalCategoryType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'required' => false,
'model' => 'category.category',
'multiple' => true,
'ajax_lookup_action' => function (Options $options) {
$query = [
'for_lookup' => 1,
];
return 'lead:getLookupChoiceList&'.http_build_query($query);
},
'model_lookup_method' => 'getLookupResults',
'lookup_arguments' => function (Options $options) {
return [
'type' => 'global',
'filter' => '$data',
'limit' => 10,
'start' => 0,
'options' => [
'is_published' => $options['is_published'],
'for_lookup' => 1,
],
];
},
'is_published' => true,
]
);
}
public function getParent(): string
{
return EntityLookupType::class;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class HtmlType extends AbstractType
{
public function getParent(): string
{
return TextareaType::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CategoryBundle\Model\CategoryModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class LeadCategoryType extends AbstractType
{
public function __construct(
private CategoryModel $categoryModel,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => function (Options $options): array {
$categories = $this->categoryModel->getLookupResults('email', '', 0);
$choices = [];
foreach ($categories as $cat) {
$choices[$cat['title']] = $cat['id'];
}
return $choices;
},
'global_only' => true,
'required' => false,
]);
}
public function getParent(): ?string
{
return ChoiceType::class;
}
public function getBlockPrefix(): string
{
return 'leadcategory_choices';
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Helper\ArrayHelper;
use Mautic\LeadBundle\Model\FieldModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class LeadFieldsType extends AbstractType
{
public function __construct(
protected FieldModel $fieldModel,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => function (Options $options): array {
$fieldList = ArrayHelper::flipArray($this->fieldModel->getFieldList());
if ($options['with_tags']) {
$fieldList['Core']['mautic.lead.field.tags'] = 'tags';
}
if ($options['with_company_fields']) {
$fieldList['Company'] = array_flip($this->fieldModel->getFieldList(false, true, ['isPublished' => true, 'object' => 'company']));
}
if ($options['with_utm']) {
$fieldList['UTM']['mautic.lead.field.utmcampaign'] = 'utm_campaign';
$fieldList['UTM']['mautic.lead.field.utmcontent'] = 'utm_content';
$fieldList['UTM']['mautic.lead.field.utmmedium'] = 'utm_medium';
$fieldList['UTM']['mautic.lead.field.umtsource'] = 'utm_source';
$fieldList['UTM']['mautic.lead.field.utmterm'] = 'utm_term';
}
return $fieldList;
},
'global_only' => false,
'required' => false,
'with_company_fields' => false,
'with_tags' => false,
'with_utm' => false,
]);
}
public function getParent(): ?string
{
return ChoiceType::class;
}
public function getBlockPrefix(): string
{
return 'leadfields_choices';
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Form\Type\UserListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<mixed>
*/
class LeadImportFieldType extends AbstractType
{
public function __construct(
private TranslatorInterface $translator,
private EntityManager $entityManager,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [];
foreach ($options['all_fields'] as $optionGroup => $fields) {
$choices[$optionGroup] = array_flip($fields);
}
foreach ($options['import_fields'] as $field => $label) {
$builder->add(
$field,
ChoiceType::class,
[
'choices' => $choices,
'label' => $label,
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'data' => $this->getDefaultValue($field, $options['import_fields']),
]
);
}
$transformer = new IdToEntityModelTransformer($this->entityManager, User::class);
$builder->add(
$builder->create(
'owner',
UserListType::class,
[
'label' => 'mautic.lead.lead.field.owner',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
'multiple' => false,
]
)
->addModelTransformer($transformer)
);
if ('lead' === $options['object']) {
$builder->add(
$builder->create(
'list',
LeadListType::class,
[
'label' => 'mautic.lead.lead.field.list',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
'multiple' => false,
]
)
);
$builder->add(
'tags',
TagType::class,
[
'label' => 'mautic.lead.tags',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'data-placeholder' => $this->translator->trans('mautic.lead.tags.select_or_create'),
'data-no-results-text' => $this->translator->trans('mautic.lead.tags.enter_to_create'),
'data-allow-add' => 'true',
'onchange' => 'Mautic.createLeadTag(this)',
],
]
);
}
$builder->add(
'skip_if_exists',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.import.skip_if_exists',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
'data' => false,
]
);
$buttons = ['cancel_icon' => 'ri-close-line'];
if (empty($options['line_count_limit'])) {
$buttons = array_merge(
$buttons,
[
'apply_text' => 'mautic.lead.import.in.background',
'apply_class' => 'btn btn-secondary',
'apply_icon' => 'ri-history-line',
'save_text' => 'mautic.lead.import.start',
'save_class' => 'btn btn-secondary',
'save_icon' => 'ri-import-line',
]
);
} else {
$buttons = array_merge(
$buttons,
[
'apply_text' => false,
'save_text' => 'mautic.lead.import',
'save_class' => 'btn btn-primary',
'save_icon' => 'ri-import-line',
]
);
}
$builder->add('buttons', FormButtonsType::class, $buttons);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['all_fields', 'import_fields', 'object']);
$resolver->setDefaults([
'line_count_limit' => 0,
'validation_groups' => [
User::class,
'determineValidationGroups',
],
]);
}
public function getBlockPrefix(): string
{
return 'lead_field_import';
}
/**
* @param string $fieldName
*
* @return string
*/
public function getDefaultValue($fieldName, array $importFields)
{
return $importFields[$fieldName] ?? null;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Validator\Constraints\FileEncoding as EncodingValidation;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class LeadImportType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'file',
FileType::class,
[
'label' => 'mautic.lead.import.file',
'attr' => [
'accept' => '.csv',
'class' => 'form-control',
],
'constraints' => [
new File(
[
'mimeTypes' => ['text/*', 'application/octet-stream', 'application/csv'],
'mimeTypesMessage' => 'mautic.core.invalid_file_type',
]
),
new EncodingValidation(
[
'encodingFormat' => ['UTF-8'],
'encodingFormatMessage' => 'mautic.core.invalid_file_encoding',
]
),
new NotBlank(
['message' => 'mautic.import.file.required']
),
],
'error_bubbling' => true,
]
);
$constraints = [
new NotBlank(
['message' => 'mautic.core.value.required']
),
];
$default = (empty($options['data']['delimiter'])) ? ',' : htmlspecialchars($options['data']['delimiter']);
$builder->add(
'delimiter',
TextType::class,
[
'label' => 'mautic.lead.import.delimiter',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.import.delimiter.help',
],
'data' => $default,
'constraints' => $constraints,
]
);
$default = (empty($options['data']['enclosure'])) ? '"' : htmlspecialchars($options['data']['enclosure']);
$builder->add(
'enclosure',
TextType::class,
[
'label' => 'mautic.lead.import.enclosure',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.import.enclosure.help',
],
'data' => $default,
'constraints' => $constraints,
]
);
$default = (empty($options['data']['escape'])) ? '\\' : $options['data']['escape'];
$builder->add(
'escape',
TextType::class,
[
'label' => 'mautic.lead.import.escape',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.import.escape.help',
],
'data' => $default,
'constraints' => $constraints,
]
);
$default = (empty($options['data']['batchlimit'])) ? 100 : (int) $options['data']['batchlimit'];
$builder->add(
'batchlimit',
TextType::class,
[
'label' => 'mautic.lead.import.batchlimit',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.import.batchlimit_tooltip',
],
'data' => $default,
'constraints' => $constraints,
]
);
$builder->add(
'start',
SubmitType::class,
[
'attr' => [
'class' => 'btn btn-tertiary',
'icon' => 'ri-import-line',
'onclick' => "mQuery(this).prop('disabled', true); mQuery('form[name=\'lead_import\']').submit();",
],
'label' => 'mautic.lead.import.upload',
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Model\ListModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class LeadListType extends AbstractType
{
public function __construct(
private ListModel $segmentModel,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => function (Options $options): array {
$lists = (empty($options['global_only'])) ? $this->segmentModel->getUserLists() : $this->segmentModel->getGlobalLists();
$lists = (empty($options['preference_center_only'])) ? $lists : $this->segmentModel->getPreferenceCenterLists();
$choices = [];
foreach ($lists as $l) {
if (empty($options['preference_center_only'])) {
$choices[$l['name'].' ('.$l['id'].')'] = $l['id'];
} else {
$choices[empty($l['publicName']) ? $l['name'].' ('.$l['id'].')' : $l['publicName'].' ('.$l['id'].')'] = $l['id'];
}
}
return $choices;
},
'global_only' => false,
'preference_center_only' => false,
'required' => false,
]);
}
public function getParent(): ?string
{
return ChoiceType::class;
}
public function getBlockPrefix(): string
{
return 'leadlist_choices';
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\StageBundle\Entity\Stage;
use Mautic\StageBundle\Form\Type\StageListType;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Form\Type\UserListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<Lead>
*/
class LeadType extends AbstractType
{
use EntityFieldsBuildFormTrait;
public function __construct(
private TranslatorInterface $translator,
private CompanyModel $companyModel,
private EntityManager $entityManager,
private CoreParametersHelper $coreParametersHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new FormExitSubscriber('lead.lead', $options));
if (!$options['isShortForm']) {
$imageChoices = [
'Gravatar' => 'gravatar',
'mautic.lead.lead.field.custom_avatar' => 'custom',
];
$cache = $options['data']->getSocialCache() ?? [];
if (count($cache)) {
foreach ($cache as $key => $data) {
$imageChoices[$key] = $key;
}
}
$builder->add(
'preferred_profile_image',
ChoiceType::class,
[
'choices' => $imageChoices,
'label' => 'mautic.lead.lead.field.preferred_profile',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => true,
'multiple' => false,
]
);
$builder->add(
'custom_avatar',
FileType::class,
[
'label' => false,
'label_attr' => ['class' => 'control-label'],
'required' => false,
'attr' => [
'class' => 'form-control',
],
'mapped' => false,
'constraints' => [
new File(
[
'mimeTypes' => [
'image/gif',
'image/jpeg',
'image/png',
],
'mimeTypesMessage' => 'mautic.lead.avatar.types_invalid',
]
),
],
]
);
}
$cleaningRules = $this->getFormFields($builder, $options);
$cleaningRules['email'] = 'email';
$builder->add(
'tags',
TagType::class,
[
'by_reference' => false,
'attr' => [
'id' => 'lead_tags',
'data-placeholder' => $this->translator->trans('mautic.lead.tags.select_or_create'),
'data-no-results-text' => $this->translator->trans('mautic.lead.tags.enter_to_create'),
'data-allow-add' => 'true',
'onchange' => 'Mautic.createLeadTag(this)',
'autocomplete' => 'off',
'multiple' => 'multiple',
'aria-label' => $this->translator->trans('mautic.lead.tags.aria.label'),
'aria-describedby' => 'lead_tags_help',
'aria-expanded' => 'false',
'role' => 'combobox',
'aria-multiselectable' => 'true',
],
]
);
$allowMultipleCompanies = $this->coreParametersHelper->get('contact_allow_multiple_companies');
$companyIds = $this->companyModel->getCompanyLeadRepository()->getCompanyIdsByLeadId((string) $options['data']->getId());
$builder->add(
'companies',
CompanyListType::class,
[
'label' => 'mautic.company.selectcompany',
'label_attr' => ['class' => 'control-label'],
'multiple' => $allowMultipleCompanies,
'required' => false,
'mapped' => false,
'data' => !$allowMultipleCompanies ? ($companyIds[0] ?? null) : array_combine($companyIds, $companyIds),
]
);
$transformer = new IdToEntityModelTransformer($this->entityManager, User::class);
$builder->add(
$builder->create(
'owner',
UserListType::class,
[
'label' => 'mautic.lead.lead.field.owner',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
'multiple' => false,
]
)
->addModelTransformer($transformer)
);
$transformer = new IdToEntityModelTransformer($this->entityManager, Stage::class);
$builder->add(
$builder->create(
'stage',
StageListType::class,
[
'label' => 'mautic.lead.lead.field.stage',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'required' => false,
'multiple' => false,
]
)
->addModelTransformer($transformer)
);
if (!$options['isShortForm']) {
$builder->add('buttons', FormButtonsType::class);
} else {
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
]
);
}
$builder->addEventSubscriber(new CleanFormSubscriber($cleaningRules));
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => Lead::class,
'isShortForm' => false,
]
);
$resolver->setRequired(['fields', 'isShortForm']);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class ListActionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'addToLists',
LeadListType::class,
[
'label' => 'mautic.lead.lead.events.addtolists',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'multiple' => true,
'expanded' => false,
]
);
$builder->add(
'removeFromLists',
LeadListType::class,
[
'label' => 'mautic.lead.lead.events.removefromlists',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'multiple' => true,
'expanded' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'leadlist_action';
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\CoreBundle\Form\Validator\Constraints\CircularDependency;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Form\DataTransformer\FieldFilterTransformer;
use Mautic\LeadBundle\Model\ListModel;
use Mautic\LeadBundle\Validator\Constraints\SegmentDate;
use Mautic\ProjectBundle\Form\Type\ProjectType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<LeadList>
*/
class ListType extends AbstractType
{
public function __construct(
private TranslatorInterface $translator,
private ListModel $listModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html', 'name' => 'string', 'publicName' => 'string', 'filter' => 'raw']));
$builder->addEventSubscriber(new FormExitSubscriber('lead.list', $options));
$builder->add(
'name',
TextType::class,
[
'label' => 'mautic.core.name',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add(
'publicName',
TextType::class,
[
'label' => 'mautic.lead.list.form.publicname',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.list.form.publicname.tooltip',
'placeholder' => 'mautic.core.autogenerated',
],
'required' => false,
]
);
$builder->add(
'alias',
TextType::class,
[
'label' => 'mautic.core.alias',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'length' => 25,
'tooltip' => 'mautic.lead.list.help.alias',
'placeholder' => 'mautic.core.autogenerated',
],
'required' => false,
]
);
$builder->add(
'description',
TextareaType::class,
[
'label' => 'mautic.core.description',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control editor'],
'required' => false,
]
);
$builder->add(
'category',
CategoryListType::class,
[
'bundle' => 'segment',
]
);
$builder->add(
'isGlobal',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.list.form.isglobal',
'attr' => [
'tooltip' => 'mautic.lead.list.form.isglobal.tooltip',
],
'no_label' => 'mautic.lead.list.form.isglobal.no',
]
);
$builder->add(
'isPreferenceCenter',
YesNoButtonGroupType::class,
[
'label' => 'mautic.lead.list.form.isPreferenceCenter',
'attr' => [
'tooltip' => 'mautic.lead.list.form.isPreferenceCenter.tooltip',
],
]
);
$builder->add('projects', ProjectType::class);
$builder->add('isPublished', YesNoButtonGroupType::class);
$filterModalTransformer = new FieldFilterTransformer($this->translator, ['object' => 'lead']);
$builder->add(
$builder->create(
'filters',
CollectionType::class,
[
'entry_type' => FilterType::class,
'error_bubbling' => false,
'mapped' => true,
'allow_add' => true,
'allow_delete' => true,
'label' => false,
'constraints' => [
new CircularDependency([
'message' => 'mautic.core.segment.circular_dependency_exists',
]),
new SegmentDate([
'message' => 'mautic.lead.segment.date_invalid',
]),
],
]
)->addModelTransformer($filterModalTransformer)
);
$builder->add('buttons', FormButtonsType::class);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'data_class' => LeadList::class,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['fields'] = $this->listModel->getChoiceFields();
}
public function getBlockPrefix(): string
{
return 'leadlist';
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<mixed>
*/
class MergeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'lead_to_merge',
ChoiceType::class,
[
'choices' => $options['leads'],
'label' => 'mautic.lead.merge.select',
'label_attr' => ['class' => 'control-label'],
'multiple' => false,
'placeholder' => '',
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.merge.select.modal.tooltip',
],
'constraints' => [
new NotBlank(
[
'message' => 'mautic.core.value.required',
]
),
],
]
);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.lead.merge',
'save_icon' => 'ri-user-6-line',
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['leads']);
}
public function getBlockPrefix(): string
{
return 'lead_merge';
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @extends AbstractType<mixed>
*/
class ModifyLeadTagsType extends AbstractType
{
public function __construct(
private TranslatorInterface $translator,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'add_tags',
TagType::class,
[
'label' => 'mautic.lead.tags.add',
'attr' => [
'data-placeholder' => $this->translator->trans('mautic.lead.tags.select_or_create'),
'data-no-results-text' => $this->translator->trans('mautic.lead.tags.enter_to_create'),
'data-allow-add' => 'true',
'onchange' => 'Mautic.createLeadTag(this)',
],
'data' => $options['data']['add_tags'] ?? null,
'add_transformer' => true,
]
);
$builder->add(
'remove_tags',
TagType::class,
[
'label' => 'mautic.lead.tags.remove',
'attr' => [
'data-placeholder' => $this->translator->trans('mautic.lead.tags.select_or_create'),
'data-no-results-text' => $this->translator->trans('mautic.lead.tags.enter_to_create'),
'data-allow-add' => 'true',
'onchange' => 'Mautic.createLeadTag(this)',
],
'data' => $options['data']['remove_tags'] ?? null,
'add_transformer' => true,
]
);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\LeadBundle\Entity\LeadNote;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<LeadNote>
*/
class NoteType extends AbstractType
{
private DateTimeHelper $dateHelper;
public function __construct()
{
$this->dateHelper = new DateTimeHelper();
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CleanFormSubscriber(['text' => 'html']));
$builder->addEventSubscriber(new FormExitSubscriber('lead.note', $options));
$builder->add(
'text',
TextareaType::class,
[
'label' => 'mautic.lead.note.form.text',
'label_attr' => ['class' => 'control-label sr-only'],
'attr' => ['class' => 'mousetrap form-control editor', 'rows' => 10, 'autofocus' => 'autofocus'],
]
);
$builder->add(
'type',
ChoiceType::class,
[
'label' => 'mautic.lead.note.form.type',
'choices' => [
'mautic.lead.note.type.general' => 'general',
'mautic.lead.note.type.email' => 'email',
'mautic.lead.note.type.call' => 'call',
'mautic.lead.note.type.meeting' => 'meeting',
],
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$dt = $options['data']->getDatetime();
$data = (null == $dt) ? $this->dateHelper->getDateTime() : $dt;
$builder->add(
'dateTime',
DateTimeType::class,
[
'label' => 'mautic.core.date.added',
'label_attr' => ['class' => 'control-label'],
'widget' => 'single_text',
'attr' => [
'class' => 'form-control',
'data-toggle' => 'datetime',
'preaddon' => 'ri-calendar-line',
],
'format' => 'yyyy-MM-dd HH:mm',
'html5' => false,
'data' => $data,
]
);
$builder->add('buttons', FormButtonsType::class, [
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
]);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => LeadNote::class,
]);
}
public function getBlockPrefix(): string
{
return 'leadnote';
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\ButtonGroupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class NullableYesNoButtonGroupType extends AbstractType
{
public function getParent(): string
{
return ButtonGroupType::class;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'choices' => fn (Options $options): array => [
$options['no_label'] => $options['no_value'],
$options['yes_label'] => $options['yes_value'],
],
'choice_value' => function ($choiceKey) {
if (null === $choiceKey || '' === $choiceKey) {
return null;
}
return (is_string($choiceKey) && !is_numeric($choiceKey)) ? $choiceKey : (int) $choiceKey;
},
'expanded' => true,
'multiple' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'label' => 'mautic.core.form.active',
'placeholder' => true,
'required' => false,
'no_label' => 'mautic.core.form.no',
'no_value' => 0,
'yes_label' => 'mautic.core.form.yes',
'yes_value' => 1,
'empty_data' => null,
'data' => null,
]
);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class OwnerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'addowner',
ChoiceType::class,
[
'label' => 'mautic.lead.batch.add_to',
'multiple' => false,
'choices' => $options['items'],
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add('ids', HiddenType::class);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(
[
'items',
]
);
}
public function getBlockPrefix(): string
{
return 'lead_batch_owner';
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\PointBundle\Form\Type\GroupListType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\NotEqualTo;
/**
* @extends AbstractType<mixed>
*/
class PointActionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'points',
NumberType::class,
[
'label' => 'mautic.lead.lead.event.points',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'scale' => 0,
'data' => $options['data']['points'] ?? 0,
'constraints' => [
new NotEqualTo(
[
'value' => '0',
'message' => 'mautic.core.value.required',
]
),
],
]
);
$builder->add('group', GroupListType::class, [
'label' => 'mautic.lead.campaign.event.point_group',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.campaign.event.point_group.help',
],
'required' => false,
'by_reference' => false,
'return_entity' => false,
]);
}
public function getBlockPrefix(): string
{
return 'leadpoints_action';
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\LeadBundle\Model\LeadModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class PreferenceChannelsType extends AbstractType
{
public function __construct(
private LeadModel $leadModel,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$model = $this->leadModel;
$resolver->setDefaults(
[
'choices' => fn (Options $options) => $model->getPreferenceChannels(),
'placeholder' => '',
'attr' => ['class' => 'form-control'],
'label_attr' => ['class' => 'control-label'],
'multiple' => false,
'expanded' => false,
'required' => false,
]
);
}
public function getParent(): ?string
{
return ChoiceType::class;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class SegmentConfigType extends AbstractType
{
/**
* @param FormBuilderInterface<FormBuilderInterface> $builder
* @param mixed[] $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'segment_rebuild_time_warning',
NumberType::class,
[
'label' => 'mautic.lead.list.form.config.segment_rebuild_time_warning',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.list.form.config.segment_rebuild_time_warning.tooltip',
],
'required' => false,
]
);
$builder->add(
'segment_build_time_warning',
NumberType::class,
[
'label' => 'mautic.lead.list.form.config.segment_build_time_warning',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.lead.list.form.config.segment_build_time_warning.tooltip',
],
'required' => false,
]
);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<mixed>
*/
class StageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'addstage',
ChoiceType::class,
[
'label' => 'mautic.lead.batch.add_to',
'multiple' => false,
'choices' => $options['items'],
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
$builder->add('ids', HiddenType::class);
$builder->add(
'buttons',
FormButtonsType::class,
[
'apply_text' => false,
'save_text' => 'mautic.core.form.save',
'cancel_onclick' => 'javascript:void(0);',
'cancel_attr' => [
'data-dismiss' => 'modal',
],
]
);
if (!empty($options['action'])) {
$builder->setAction($options['action']);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(
[
'items',
]
);
}
public function getBlockPrefix(): string
{
return 'lead_batch_stage';
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class TagEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('tag', TextType::class);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Mautic\LeadBundle\Entity\Tag;
use Mautic\LeadBundle\Form\DataTransformer\TagEntityModelTransformer;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<Tag>
*/
class TagType extends AbstractType
{
public function __construct(
private EntityManager $em,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['add_transformer']) {
$transformer = new TagEntityModelTransformer(
$this->em,
Tag::class,
$options['multiple']
);
$builder->addModelTransformer($transformer);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'label' => 'mautic.lead.tags',
'class' => Tag::class,
'query_builder' => fn (EntityRepository $er) => $er->createQueryBuilder('t')->orderBy('t.tag', Order::Ascending->value),
'choice_label' => 'tag',
'multiple' => true,
'required' => false,
'disabled' => false,
'add_transformer' => false,
]
);
}
public function getBlockPrefix(): string
{
return 'lead_tag';
}
public function getParent(): ?string
{
return EntityType::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Model\FieldModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class UpdateCompanyActionType extends AbstractType
{
use EntityFieldsBuildFormTrait;
public function __construct(
protected FieldModel $fieldModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$leadFields = $this->fieldModel->getEntities(
[
'force' => [
[
'column' => 'f.isPublished',
'expr' => 'eq',
'value' => true,
],
],
'hydration_mode' => 'HYDRATE_ARRAY',
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
]
);
$options['fields'] = $leadFields;
$options['ignore_required_constraints'] = true;
$options['use_nullable_yes_no_type'] = true;
$this->getFormFields($builder, $options, 'company');
}
public function getBlockPrefix(): string
{
return 'updatecompany_action';
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Mautic\LeadBundle\Form\Type;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Model\FieldModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<mixed>
*/
class UpdateLeadActionType extends AbstractType
{
use EntityFieldsBuildFormTrait;
public function __construct(
private FieldModel $fieldModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$leadFields = $this->fieldModel->getEntities(
[
'force' => [
[
'column' => 'f.isPublished',
'expr' => 'eq',
'value' => true,
],
],
'hydration_mode' => 'HYDRATE_ARRAY',
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
]
);
$options['fields'] = $leadFields;
$options['ignore_required_constraints'] = true;
$options['ignore_date_type'] = true;
$options['use_nullable_yes_no_type'] = true;
$this->getFormFields($builder, $options);
}
public function getBlockPrefix(): string
{
return 'updatelead_action';
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
final class DbRegex extends Constraint
{
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class DbRegexValidator extends ConstraintValidator
{
public function __construct(private Connection $connection)
{
}
public function validate(mixed $regex, Constraint $constraint): void
{
if (!$constraint instanceof DbRegex) {
throw new UnexpectedTypeException($constraint, DbRegex::class);
}
try {
$this->connection->executeQuery('SELECT 1 REGEXP ? AS is_valid', [$regex]);
} catch (Exception $e) {
$this->context->buildViolation(
$this->stripUglyPartOfTheErrorMessage($e->getPrevious()->getMessage())
)->addViolation();
}
}
private function stripUglyPartOfTheErrorMessage(string $message): string
{
return preg_replace('/SQLSTATE\[\d+\]: [\w ]+: \d+ /', '', $message);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class EmailAddress extends Constraint
{
public function validatedBy(): string
{
return static::class.'Validator';
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Mautic\EmailBundle\Exception\InvalidEmailException;
use Mautic\EmailBundle\Helper\EmailValidator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class EmailAddressValidator extends ConstraintValidator
{
public function __construct(
private EmailValidator $emailValidator,
) {
}
/**
* @param mixed $value
*/
public function validate($value, Constraint $constraint): void
{
if (!empty($value)) {
try {
$this->emailValidator->validate($value);
} catch (InvalidEmailException $invalidEmailException) {
$this->context->addViolation(
$invalidEmailException->getMessage()
);
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class FieldAliasKeyword extends Constraint
{
public $message = 'mautic.lead.field.keyword.invalid';
public function validatedBy(): string
{
return FieldAliasKeywordValidator::class;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Doctrine\ORM\EntityManager;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Helper\FieldAliasHelper;
use Mautic\LeadBundle\Model\ListModel;
use Mautic\LeadBundle\Services\ContactSegmentFilterDictionary;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Throws an exception if the field alias is equal some segment filter keyword.
* It would cause odd behavior with segment filters otherwise.
*/
class FieldAliasKeywordValidator extends ConstraintValidator
{
public const RESTRICTED_ALIASES = [
'contact_id',
'company_id',
'notes',
'owner',
'id',
'ip',
'tags',
'dateAdded',
'dateModified',
'lastActive',
'createdByUser',
'modifiedByUser',
];
public function __construct(
private ListModel $listModel,
private FieldAliasHelper $aliasHelper,
private EntityManager $em,
private TranslatorInterface $translator,
private ContactSegmentFilterDictionary $contactSegmentFilterDictionary,
) {
}
/**
* @param LeadField $field
*/
public function validate($field, Constraint $constraint): void
{
$oldValue = $this->em->getUnitOfWork()->getOriginalEntityData($field);
$this->aliasHelper->makeAliasUnique($field);
// If empty it's a new object else it's an edit
if (empty($oldValue) || (!empty($oldValue) && is_array($oldValue) && $oldValue['alias'] != $field->getAlias())) {
if (in_array($field->getAlias(), self::RESTRICTED_ALIASES)) {
$this->context->addViolation(
$this->translator->trans(
'mautic.lead.field.keyword.restricted',
['%alias%' => $field->getAlias()],
'validators'
)
);
return;
}
$choices = array_merge($this->listModel->getChoiceFields()[$field->getObject()] ?? [], $this->contactSegmentFilterDictionary->getFilters());
if (isset($choices[$field->getAlias()])) {
$this->context->addViolation($constraint->message, ['%keyword%' => $field->getAlias()]);
}
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class LeadListAccess extends Constraint
{
public string $message = 'mautic.lead.lists.failed';
public bool $allowEmpty = false;
public function validatedBy(): string
{
return 'leadlist_access';
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Mautic\LeadBundle\Model\ListModel;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class LeadListAccessValidator extends ConstraintValidator
{
public function __construct(
private ListModel $segmentModel,
) {
}
/**
* @param mixed $value
*/
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof LeadListAccess) {
throw new UnexpectedTypeException($constraint, LeadListAccess::class);
}
if (count($value)) {
$lists = $this->segmentModel->getUserLists();
foreach ($value as $l) {
if (!isset($lists[$l->getId()])) {
$this->context->addViolation(
$constraint->message,
['%string%' => $l->getName()]
);
break;
}
}
} elseif (!$constraint->allowEmpty) {
$this->context->addViolation($constraint->message);
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class SegmentInUse extends Constraint
{
public $message = 'mautic.lead_list.is_in_use';
public function validatedBy(): string
{
return 'segment_in_use';
}
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Model\ListModel;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class SegmentInUseValidator extends ConstraintValidator
{
public function __construct(
private ListModel $listModel,
) {
}
/**
* @param LeadList $leadList
*/
public function validate($leadList, Constraint $constraint): void
{
if (!$constraint instanceof SegmentInUse) {
throw new UnexpectedTypeException($constraint, SegmentInUse::class);
}
if (!$leadList->getId() || $leadList->getIsPublished()) {
return;
}
$lists = $this->listModel->getSegmentsWithDependenciesOnSegment($leadList->getId(), 'name');
if (count($lists)) {
$this->context->buildViolation($constraint->message)
->setCode((string) Response::HTTP_UNPROCESSABLE_ENTITY)
->setParameter('%segments%', implode(',', $lists))
->addViolation();
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class UniqueCustomField extends Constraint
{
public string $message = 'mautic.lead.field.unique.is_used';
public string $object;
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\LeadModel;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class UniqueCustomFieldValidator extends ConstraintValidator
{
public function __construct(
private LeadModel $leadModel,
private CompanyModel $companyModel,
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
) {
}
/**
* @param Lead|Company|mixed $object
*/
public function validate($object, Constraint $constraint): void
{
\assert($constraint instanceof UniqueCustomField);
\assert($object instanceof Lead || $object instanceof Company);
$form = $this->context->getRoot();
// When using API Platform, the root is not a Form instance
if (!$form instanceof Form) {
return;
}
$publishedUniqueFields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier([
'isPublished' => true,
'isUniqueIdentifer' => true,
'object' => $constraint->object,
]);
$publishedUniqueFields = array_keys($publishedUniqueFields);
$uniqueFieldsData = [];
foreach ($publishedUniqueFields as $publishedUniqueField) {
if (!$form->has($publishedUniqueField)) {
continue;
}
$data = $form->get($publishedUniqueField)->getData();
if (null === $data || '' === $data) {
continue;
}
$uniqueFieldsData[$publishedUniqueField] = $data;
}
$validatedFields = [];
if ($object instanceof Lead) {
$validatedFields = $this->getLeadFieldsValid($object, $uniqueFieldsData);
}
if ($object instanceof Company) {
$validatedFields = $this->getCompanyFieldsValid($object, $uniqueFieldsData);
}
foreach ($validatedFields as $fieldName => $isValid) {
if ($isValid) {
continue;
}
$this->context->buildViolation($constraint->message)
->setCode((string) Response::HTTP_UNPROCESSABLE_ENTITY)
->atPath($fieldName)
->addViolation();
}
}
/**
* @param array<mixed> $fieldsData
*
* @return array<bool>
*/
private function getLeadFieldsValid(Lead $lead, array $fieldsData): array
{
$leadRepository = $this->leadModel->getRepository();
if ('orWhere' === $leadRepository->getUniqueIdentifiersWherePart()) {
$fieldsValidation = [];
foreach ($fieldsData as $field => $data) {
$leads = $leadRepository->getLeadIdsByUniqueFields([$field => $data]);
$fieldsValidation[] = $this->isValid($leads, [$field], (int) $lead->getId());
}
return array_merge(...$fieldsValidation);
}
// Can't use getEntities, because it refreshes some field data, that can be used in the form
$leads = $leadRepository->getLeadIdsByUniqueFields($fieldsData);
return $this->isValid($leads, array_keys($fieldsData), (int) $lead->getId());
}
/**
* @param array<mixed> $fieldsData
*
* @return array<bool>
*/
private function getCompanyFieldsValid(Company $company, array $fieldsData): array
{
$companyRepository = $this->companyModel->getRepository();
if ('orWhere' === $companyRepository->getUniqueIdentifiersWherePart()) {
$fieldsValidation = [];
foreach ($fieldsData as $field => $data) {
$companies = $companyRepository->getCompanyIdsByUniqueFields([$field => $data]);
$fieldsValidation[] = $this->isValid($companies, [$field], (int) $company->getId());
}
return array_merge(...$fieldsValidation);
}
// Can't use getEntities, because it refreshes some field data, that can be used in the form
$companies = $companyRepository->getCompanyIdsByUniqueFields($fieldsData);
return $this->isValid($companies, array_keys($fieldsData), (int) $company->getId());
}
/**
* @param array<array<mixed>> $objects
* @param array<string> $fields
*
* @return array<bool>
*/
private function isValid(array $objects, array $fields, int $objectId): array
{
$objectsCount = count($objects);
if (0 === $objectsCount) {
return array_fill_keys($fields, true);
}
if ($objectsCount > 1) {
return array_fill_keys($fields, false);
}
if ((int) $objects[0]['id'] === $objectId) {
return array_fill_keys($fields, true);
}
return array_fill_keys($fields, false);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class UniqueUserAlias extends Constraint
{
public $message = 'This alias is already in use.';
public $field = '';
public function validatedBy(): string
{
return 'uniqueleadlist';
}
public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
}
public function getRequiredOptions(): array
{
return ['field'];
}
public function getDefaultOption(): ?string
{
return 'field';
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Mautic\LeadBundle\Form\Validator\Constraints;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\LeadBundle\Entity\LeadListRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
class UniqueUserAliasValidator extends ConstraintValidator
{
/**
* @var LeadListRepository
*/
public $segmentRepository;
/**
* @var UserHelper
*/
public $userHelper;
public function __construct(LeadListRepository $segmentRepository, UserHelper $userHelper)
{
$this->segmentRepository = $segmentRepository;
$this->userHelper = $userHelper;
}
public function validate(mixed $list, Constraint $constraint): void
{
$field = $constraint->field;
if (empty($field)) {
throw new ConstraintDefinitionException('A field has to be specified.');
}
if ($list->getAlias()) {
$lists = $this->segmentRepository->getLists(
$this->userHelper->getUser(),
$list->getAlias(),
$list->getId()
);
if (count($lists)) {
$this->context->buildViolation($constraint->message)
->atPath($field)
->setParameter('%alias%', $list->getAlias())
->addViolation();
}
}
}
}