Skip to content

Commit

Permalink
refactor: validate link form + anchor links (#863)
Browse files Browse the repository at this point in the history
Co-authored-by: David mattei <[email protected]>
  • Loading branch information
theus77 and Davidmattei authored Apr 6, 2024
1 parent 4bda8e5 commit 759371e
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 9 deletions.
21 changes: 21 additions & 0 deletions EMS/admin-ui-bundle/assets/js/core/events/formFailEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const EMS_FORM_FAIL_EVENT_EVENT = 'emsFormFailEvent'
export class FormFailEvent {
constructor (form, response) {
this._form = form

this._event = new CustomEvent(EMS_FORM_FAIL_EVENT_EVENT, {
detail: {
form,
response
}
})
}

dispatch () {
if (undefined === this._form) {
return
}
this._form.dispatchEvent(this._event)
}
}
export default FormFailEvent
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,7 @@ export default class LinkUI extends Plugin {
const linkCommand = editor.commands.get('link')
const value = linkCommand.value || ''
const target = undefined !== linkCommand.target ? linkCommand.target : null

this.formModal.show(value, target)
this.formModal.show(value, target, editor.getData())
}

/**
Expand Down
14 changes: 10 additions & 4 deletions EMS/admin-ui-bundle/assets/js/core/helpers/linkModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AddedDomEvent from '../events/addedDomEvent'
import SelectLinkEvent from '../events/selectLinkEvent'
import Link from './link'
import { EMS_FORM_RESPONSE_EVENT_EVENT } from '../events/formResponseEvent'
import { EMS_FORM_FAIL_EVENT_EVENT } from '../events/formFailEvent'

export default class LinkModal {
constructor () {
Expand All @@ -13,9 +14,9 @@ export default class LinkModal {
})
}

show (value, target = null) {
show (value, target = null, content = null) {
this.modal.show()
this._loadModal(value, target)
this._loadModal(value, target, content)
}

setLoading (showLoading) {
Expand All @@ -39,10 +40,10 @@ export default class LinkModal {
return this.modal._isShown
}

_loadModal (value, target) {
_loadModal (value, target, content) {
const self = this
const link = new Link(value)
ajaxRequest.post(this.linkModal.dataset.modalInitUrl, { url: link.href, target })
ajaxRequest.post(this.linkModal.dataset.modalInitUrl, { url: link.href, target, content })
.success(response => self._treatResponse(response))
}

Expand All @@ -56,6 +57,7 @@ export default class LinkModal {
const forms = body.querySelectorAll('form')
for (let i = 0; i < forms.length; ++i) {
forms[i].addEventListener(EMS_FORM_RESPONSE_EVENT_EVENT, (event) => self._onResponse(event))
forms[i].addEventListener(EMS_FORM_FAIL_EVENT_EVENT, (event) => self._onFail(event))
}
}

Expand All @@ -64,4 +66,8 @@ export default class LinkModal {
const selectEvent = new SelectLinkEvent(event.detail.response.url, event.detail.response.target)
selectEvent.dispatch()
}

_onFail (event) {
this._treatResponse(event.detail.response)
}
}
5 changes: 5 additions & 0 deletions EMS/admin-ui-bundle/assets/js/core/plugins/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ChangeEvent from '../events/changeEvent'
import DynamicForm from '../helpers/dynamic-form'
import { EMS_CTRL_SAVE_EVENT } from '../events/ctrlSaveEvent'
import { FormResponseEvent } from '../events/formResponseEvent'
import { FormFailEvent } from '../events/formFailEvent'
import '../../../css/core/components/form.scss'

class Form {
Expand Down Expand Up @@ -43,6 +44,10 @@ class Form {
const event = new FormResponseEvent(form.get(0), response)
event.dispatch()
})
.fail(function (response) {
const event = new FormFailEvent(form.get(0), response)
event.dispatch()
})
}

button.on('click', ajaxSave)
Expand Down
41 changes: 39 additions & 2 deletions EMS/core-bundle/src/Controller/Wysiwyg/ModalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
use EMS\CoreBundle\Entity\Form\LoadLinkModalEntity;
use EMS\CoreBundle\Form\Form\LoadLinkModalType;
use EMS\CoreBundle\Service\Revision\RevisionService;
use EMS\Helpers\Html\HtmlHelper;
use EMS\Helpers\Standard\Json;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Twig\Environment;

class ModalController extends AbstractController
Expand All @@ -25,18 +30,45 @@ public function loadLinkModal(Request $request): JsonResponse
{
$url = (string) $request->request->get('url', '');
$target = (string) $request->request->get('target', '');
$content = (string) $request->request->get('content', '');
$targets = [];
if (HtmlHelper::isHtml($content)) {
$crawler = new Crawler($content);
foreach ($crawler->filter('[id]') as $tag) {
if (null === $tag->attributes) {
continue;
}
$node = $tag->attributes->getNamedItem('id');
if (null === $node) {
continue;
}
$id = $node->nodeValue;
$targets[$id] = "#$id";
}
}
$anchorTargets = $request->query->get('anchorTargets');
if (empty($targets) && \is_string($anchorTargets)) {
$targets = Json::decode($anchorTargets);
}

$loadLinkModalEntity = new LoadLinkModalEntity($url, $target);
$form = $this->createForm(LoadLinkModalType::class, $loadLinkModalEntity, [
LoadLinkModalType::WITH_TARGET_BLANK_FIELD => $loadLinkModalEntity->hasTargetBlank(),
LoadLinkModalType::ANCHOR_TARGETS => $targets,
'constraints' => [
new Callback($this->validate(...)),
],
]);

$form->handleRequest($request);
$response = [
'body' => $this->twig->render("@$this->templateNamespace/modal/link.html.twig", [
'form' => $form->createView(),
]),
];
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($form->isSubmitted() && !$form->isValid()) {
$response['success'] = false;
} elseif ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
if (!$data instanceof LoadLinkModalEntity) {
throw new \RuntimeException('Unexpected not LoadLinkModalEntity submitted data');
Expand All @@ -60,4 +92,9 @@ public function emsLinkInfo(Request $request): JsonResponse
'label' => $this->revisionService->display($link),
]);
}

public function validate(LoadLinkModalEntity $loadLinkModalEntity, ExecutionContextInterface $context): void
{
$loadLinkModalEntity->validate($context);
}
}
59 changes: 59 additions & 0 deletions EMS/core-bundle/src/Entity/Form/LoadLinkModalEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use EMS\CommonBundle\Helper\EmsFields;
use EMS\CoreBundle\Form\Form\LoadLinkModalType;
use EMS\Helpers\Standard\Type;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

final class LoadLinkModalEntity
{
Expand All @@ -21,6 +22,7 @@ final class LoadLinkModalEntity
private ?string $body = null;
/** @var array{sha1: string, filename: string|null, mimetype: string|null}|null */
private ?array $file = null;
private ?string $anchor = null;

public function __construct(private readonly string $url, string $target)
{
Expand All @@ -38,6 +40,9 @@ public function __construct(private readonly string $url, string $target)
$this->subject = Type::string($query['subject'] ?? '');
$this->body = Type::string($query['body'] ?? '');
$this->linkType = LoadLinkModalType::LINK_TYPE_MAILTO;
} elseif (\str_starts_with($this->url, '#')) {
$this->anchor = $this->url;
$this->linkType = LoadLinkModalType::LINK_TYPE_ANCHOR;
} else {
$this->href = $this->url;
$this->linkType = LoadLinkModalType::LINK_TYPE_URL;
Expand Down Expand Up @@ -150,6 +155,10 @@ public function generateUrl(): ?string

return "ems://asset:$hash?name=$name&type=$type";
}

return null;
case LoadLinkModalType::LINK_TYPE_ANCHOR:
return $this->anchor;
}
throw new \RuntimeException(\sprintf('Unsupported %s link type', $this->linkType));
}
Expand All @@ -169,4 +178,54 @@ public function setFile(?array $file): void
{
$this->file = $file;
}

public function getAnchor(): ?string
{
return $this->anchor;
}

public function setAnchor(?string $anchor): void
{
$this->anchor = $anchor;
}

public function validate(ExecutionContextInterface $context): void
{
switch ($this->getLinkType()) {
case LoadLinkModalType::LINK_TYPE_INTERNAL:
if ('' === ($this->dataLink ?? '')) {
$context->buildViolation('modal.link.data_link.mandatory')->atPath(LoadLinkModalType::FIELD_DATA_LINK)->addViolation();
}

return;
case LoadLinkModalType::LINK_TYPE_URL:
if ('' === ($this->url ?? '')) {
$context->buildViolation('modal.link.url.mandatory')->atPath(LoadLinkModalType::FIELD_HREF)->addViolation();
}

return;
case LoadLinkModalType::LINK_TYPE_FILE:
if (null === ($this->file[EmsFields::CONTENT_FILE_HASH_FIELD] ?? null)) {
$context->buildViolation('modal.link.file.mandatory')->atPath(LoadLinkModalType::FIELD_FILE)->addViolation();
}

return;
case LoadLinkModalType::LINK_TYPE_MAILTO:
if ('' === ($this->mailto ?? '')) {
$context->buildViolation('modal.link.mailto.mandatory')->atPath(LoadLinkModalType::FIELD_MAILTO)->addViolation();
}

return;
case LoadLinkModalType::LINK_TYPE_ANCHOR:
if ('' === ($this->anchor ?? '')) {
$context->buildViolation('modal.link.anchor.mandatory')->atPath(LoadLinkModalType::FIELD_ANCHOR)->addViolation();
} elseif (!\str_starts_with($this->anchor ?? '', '#')) {
$context->buildViolation('modal.link.anchor.format')->atPath(LoadLinkModalType::FIELD_ANCHOR)->addViolation();
}

return;
default:
$context->buildViolation('modal.link.link_type.unknown')->atPath(LoadLinkModalType::FIELD_LINK_TYPE)->addViolation();
}
}
}
30 changes: 29 additions & 1 deletion EMS/core-bundle/src/Form/Form/LoadLinkModalType.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Constraints\Email;

class LoadLinkModalType extends AbstractType
{
public const LINK_TYPE_URL = 'url';
public const LINK_TYPE_INTERNAL = 'internal';
public const LINK_TYPE_FILE = 'file';
public const LINK_TYPE_MAILTO = 'mailto';
public const LINK_TYPE_ANCHOR = 'anchor';
public const FIELD_LINK_TYPE = 'linkType';
public const FIELD_HREF = 'href';
public const FIELD_DATA_LINK = 'dataLink';
public const FIELD_MAILTO = 'mailto';
public const FIELD_SUBJECT = 'subject';
public const FIELD_BODY = 'body';
public const FIELD_FILE = 'file';
public const FIELD_ANCHOR = 'anchor';
public const FIELD_TARGET_BLANK = 'targetBlank';
public const FIELD_SUBMIT = 'submit';
public const WITH_TARGET_BLANK_FIELD = 'with_target_blank_field';
public const ANCHOR_TARGETS = 'anchor_targets';

public function __construct(private readonly RouterInterface $router)
{
Expand All @@ -55,6 +59,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'link_modal.link_type.internal' => self::LINK_TYPE_INTERNAL,
'link_modal.link_type.file' => self::LINK_TYPE_FILE,
'link_modal.link_type.mailto' => self::LINK_TYPE_MAILTO,
'link_modal.link_type.anchor' => self::LINK_TYPE_ANCHOR,
],
])
->add(self::FIELD_HREF, TextType::class, [
Expand Down Expand Up @@ -96,6 +101,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'value' => self::LINK_TYPE_MAILTO,
]]),
],
'constraints' => [
new Email(),
],
])
->add(self::FIELD_SUBJECT, TextType::class, [
'label' => 'link_modal.field.subject',
Expand Down Expand Up @@ -136,6 +144,23 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'value' => self::LINK_TYPE_FILE,
]]),
],
])
->add(self::FIELD_ANCHOR, ChoiceType::class, [
'label' => 'link_modal.field.anchor',
'attr' => ['data-tags' => true, 'class' => 'select2'],
'choices' => $options[self::ANCHOR_TARGETS],
'multiple' => false,
'choice_translation_domain' => false,
'required' => false,
'row_attr' => [
'data-show-hide' => 'show',
'data-all-any' => 'any',
'data-rules' => Json::encode([[
'field' => \sprintf('[%s]', self::FIELD_LINK_TYPE),
'condition' => 'is',
'value' => self::LINK_TYPE_ANCHOR,
]]),
],
]);

if (true === ($options[self::WITH_TARGET_BLANK_FIELD] ?? false)) {
Expand All @@ -158,7 +183,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'label' => 'link_modal.field.submit',
'attr' => [
'class' => 'btn btn-primary',
'data-ajax-save-url' => $this->router->generate(Routes::WYSIWYG_MODAL_LOAD_LINK),
'data-ajax-save-url' => $this->router->generate(Routes::WYSIWYG_MODAL_LOAD_LINK, [
'anchorTargets' => Json::encode($options[self::ANCHOR_TARGETS]),
]),
],
]);
}
Expand All @@ -168,6 +195,7 @@ public function configureOptions(OptionsResolver $resolver): void
$resolver
->setDefaults([
self::WITH_TARGET_BLANK_FIELD => false,
self::ANCHOR_TARGETS => [],
'translation_domain' => EMSCoreBundle::TRANS_FORM_DOMAIN,
'attr' => [
'class' => 'dynamic-form',
Expand Down
2 changes: 2 additions & 0 deletions EMS/core-bundle/src/Resources/translations/emsco-forms.en.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
link_modal:
field:
anchor: 'Anchor'
file: 'File'
link_type: 'Link type'
data_link: 'Internal document'
Expand All @@ -10,6 +11,7 @@ link_modal:
submit: 'Set link'
target_blank: 'Open links in a new tab (_blank)'
link_type:
anchor: 'Anchor'
url: 'URL'
internal: 'Internal Link'
file: 'File'
Expand Down
15 changes: 15 additions & 0 deletions EMS/core-bundle/src/Resources/translations/validators.en.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
modal:
link:
anchor:
mandatory: 'A target anchor is required'
format: 'An anchor must starts by a #'
mailto:
mandatory: 'A target email address is required'
url:
mandatory: 'A target URL is required'
data_link:
mandatory: 'A target document is required'
file:
mandatory: 'A target file is required'
link_type:
unknown: 'Type of link unknown'
revision:
raw_data:
version_from_required: 'This Field is not valid! Empty field'
Expand Down

0 comments on commit 759371e

Please sign in to comment.