From 614e02b4c6d56b68e0fe7f8773962944c47ba777 Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Wed, 8 Dec 2021 18:47:28 +0000 Subject: [PATCH] [PLUGIN][TagBasedFiltering] Add to user settings page and split adding tags from note/actor from editing blocked --- .../Controller/AddBlocked.php | 150 ++++++++++++++ .../Controller/EditBlocked.php | 151 ++++++++++++++ .../Controller/TagBasedFiltering.php | 196 ------------------ .../TagBasedFiltering/TagBasedFiltering.php | 24 ++- .../tag-based-filtering/edit-tags.html.twig | 23 -- .../tag_based_filtering/add_blocked.html.twig | 26 +++ .../settings_edit_blocked.html.twig | 10 + templates/settings/base.html.twig | 5 + 8 files changed, 364 insertions(+), 221 deletions(-) create mode 100644 plugins/TagBasedFiltering/Controller/AddBlocked.php create mode 100644 plugins/TagBasedFiltering/Controller/EditBlocked.php delete mode 100644 plugins/TagBasedFiltering/Controller/TagBasedFiltering.php delete mode 100644 plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig create mode 100644 plugins/TagBasedFiltering/templates/tag_based_filtering/add_blocked.html.twig create mode 100644 plugins/TagBasedFiltering/templates/tag_based_filtering/settings_edit_blocked.html.twig diff --git a/plugins/TagBasedFiltering/Controller/AddBlocked.php b/plugins/TagBasedFiltering/Controller/AddBlocked.php new file mode 100644 index 0000000000..e8bc71fedd --- /dev/null +++ b/plugins/TagBasedFiltering/Controller/AddBlocked.php @@ -0,0 +1,150 @@ +. + +// }}} + +namespace Plugin\TagBasedFiltering\Controller; + +use App\Core\Cache; +use App\Core\Controller; +use App\Core\DB\DB; +use App\Core\Form; +use function App\Core\I18n\_m; +use App\Entity\Actor; +use App\Entity\ActorTag; +use App\Entity\ActorTagBlock; +use App\Entity\Language; +use App\Entity\Note; +use App\Entity\NoteTag; +use App\Entity\NoteTagBlock; +use App\Util\Common; +use App\Util\Exception\RedirectException; +use Component\Tag\Tag; +use Functional as F; +use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\SubmitButton; +use Symfony\Component\HttpFoundation\Request; + +class AddBlocked extends Controller +{ + /** + * Edit the blocked tags of $type_name for target with ID $id. Handles both actor and note tags + */ + private function addBlocked( + Request $request, + string $type_name, + callable $calculate_target, + callable $calculate_blocks, + callable $calculate_tags, + ?string $label, + string $block_class, + ) { + $user = Common::ensureLoggedIn(); + $target = $calculate_target(); + $tag_blocks = $calculate_blocks($user); + $blockable_tags = $calculate_tags($tag_blocks); + + $form_definition = []; + foreach ($blockable_tags as $nt) { + $canon = $nt->getCanonical(); + $form_definition[] = ["{$canon}:tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']]; + $form_definition[] = ["{$canon}:use-canon", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]]; + $form_definition[] = ["{$canon}:add", SubmitType::class, ['label' => _m('Block')]]; + } + + $form = null; + if (!empty($form_definition)) { + $form = Form::create($form_definition); + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + foreach ($form_definition as [$id, $_, $opts]) { + [$canon, $type] = explode(':', $id); + if ($type === 'add') { + /** @var SubmitButton $button */ + $button = $form->get($id); + if ($button->isClicked()) { + Cache::delete($block_class::cacheKey($user->getId())); + Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); + $new_tag = Tag::ensureValid($data[$canon . ':tag']); + $language = $target instanceof Note ? Language::getByNote($target)->getLocale() : $user->getActor()->getTopLanguage()->getLocale(); + $canonical_tag = Tag::canonicalTag($new_tag, $language); + DB::persist($block_class::create([ + 'blocker' => $user->getId(), + 'tag' => $new_tag, + 'canonical' => $canonical_tag, + 'use_canonical' => $data[$canon . ':use-canon'], + ])); + DB::flush(); + throw new RedirectException; + } + } + } + } + } + + return [ + '_template' => 'tag_based_filtering/add_blocked.html.twig', + 'type' => $type_name, + $type_name => $target, + 'tags_form' => $form?->createView(), + 'label' => $label, + ]; + } + + /** + * Edit the user's list of blocked note tags, with the option of adding the notes from the note $note_id, if given + */ + public function addBlockedNoteTags(Request $request, int $note_id) + { + return self::addBlocked( + request: $request, + type_name: 'note', + calculate_target: fn () => Note::getById($note_id), + calculate_blocks: fn ($user) => NoteTagBlock::getByActorId($user->getId()), + calculate_tags: fn ($blocks) => F\reject( + NoteTag::getByNoteId($note_id), + fn (NoteTag $nt) => NoteTagBlock::checkBlocksNoteTag($nt, $blocks), + ), + label: _m('Tags in the note above:'), + block_class: NoteTagBlock::class, + ); + } + + public function addBlockedActorTags(Request $request, int $actor_id) + { + return self::addBlocked( + request: $request, + type_name: 'actor', + calculate_target: fn () => Actor::getById($actor_id), + calculate_blocks: fn ($user) => ActorTagBlock::getByActorId($user->getId()), + calculate_tags: fn ($blocks) => F\reject( + ActorTag::getByActorId($actor_id), + fn (ActorTag $nt) => ActorTagBlock::checkBlocksActorTag($nt, $blocks), + ), + label: _m('Tags of the account above:'), + block_class: ActorTagBlock::class, + ); + } +} diff --git a/plugins/TagBasedFiltering/Controller/EditBlocked.php b/plugins/TagBasedFiltering/Controller/EditBlocked.php new file mode 100644 index 0000000000..5ab4162530 --- /dev/null +++ b/plugins/TagBasedFiltering/Controller/EditBlocked.php @@ -0,0 +1,151 @@ +. + +// }}} + +namespace Plugin\TagBasedFiltering\Controller; + +use App\Core\Cache; +use App\Core\Controller; +use App\Core\DB\DB; +use App\Core\Form; +use function App\Core\I18n\_m; +use App\Entity\ActorTagBlock; +use App\Entity\NoteTagBlock; +use App\Util\Common; +use App\Util\Exception\RedirectException; +use Component\Tag\Tag; +use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\SubmitButton; +use Symfony\Component\HttpFoundation\Request; + +class EditBlocked extends Controller +{ + /** + * Worker function that handles both the note and actor tag blocking editing + */ + private static function editBlocked( + Request $request, + string $type_name, + callable $calculate_blocks, + string $label, + string $block_class, + ) { + $user = Common::ensureLoggedIn(); + $tag_blocks = $calculate_blocks($user); + + $form_definition = []; + foreach ($tag_blocks as $ntb) { + $canon = $ntb->getCanonical(); + $form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $ntb->getTag(), 'label' => ' ', 'disabled' => true]]; + $form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['label' => $ntb->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]]; + $form_definition[] = ["{$canon}:remove", SubmitType::class, ['label' => _m('Unblock')]]; + } + + $blocked_form = null; + if (!empty($form_definition)) { + $blocked_form = Form::create($form_definition); + $blocked_form->handleRequest($request); + if ($blocked_form->isSubmitted() && $blocked_form->isValid()) { + $data = $blocked_form->getData(); + foreach ($form_definition as [$id, $_, $opts]) { + [$canon, $type] = explode(':', $id); + if (\in_array($type, ['remove', 'toggle-canon'])) { + /** @var SubmitButton $button */ + $button = $blocked_form->get($id); + if ($button->isClicked()) { + Cache::delete($block_class::cacheKey($user->getId())); + Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); + switch ($type) { + case 'toggle-canon': + $ntb = DB::getReference($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]); + $ntb->setUseCanonical(!$ntb->getUseCanonical()); + DB::flush(); + throw new RedirectException; + case 'remove': + $old_tag = $data[$canon . ':old-tag']; + DB::removeBy($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]); + throw new RedirectException; + } + } + } + } + } + } + + $add_block_form = Form::create([ + ['tag', TextType::class, ['label' => ' ']], + ['use-canon', CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]], + ['add', SubmitType::class, ['label' => _m('Block')]], + ]); + + $add_block_form->handleRequest($request); + if ($add_block_form->isSubmitted() && $add_block_form->isValid()) { + $data = $add_block_form->getData(); + Cache::delete($block_class::cacheKey($user->getId())); + Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); + $new_tag = Tag::ensureValid($data['tag']); + $language = $user->getActor()->getTopLanguage()->getLocale(); + $canonical_tag = Tag::canonicalTag($new_tag, $language); + DB::persist($block_class::create([ + 'blocker' => $user->getId(), + 'tag' => $new_tag, + 'canonical' => $canonical_tag, + 'use_canonical' => $data['use-canon'], + ])); + DB::flush(); + throw new RedirectException; + } + + return [ + '_template' => 'tag_based_filtering/settings_edit_blocked.html.twig', + 'type' => $type_name, + 'blocked_form' => $blocked_form?->createView(), + 'add_block' => $add_block_form->createView(), + 'label' => $label, + ]; + } + + public static function editBlockedNoteTags(Request $request) + { + return self::editBlocked( + request: $request, + type_name: 'note', + calculate_blocks: fn ($user) => NoteTagBlock::getByActorId($user->getId()), + label: _m('Add blocked note tag:'), + block_class: NoteTagBlock::class, + ); + } + + public static function editBlockedActorTags(Request $request) + { + return self::editBlocked( + request: $request, + type_name: 'actor', + calculate_blocks: fn ($user) => ActorTagBlock::getByActorId($user->getId()), + label: _m('Add blocked people tag:'), + block_class: ActorTagBlock::class, + ); + } +} diff --git a/plugins/TagBasedFiltering/Controller/TagBasedFiltering.php b/plugins/TagBasedFiltering/Controller/TagBasedFiltering.php deleted file mode 100644 index b5dfbd76d4..0000000000 --- a/plugins/TagBasedFiltering/Controller/TagBasedFiltering.php +++ /dev/null @@ -1,196 +0,0 @@ -. - -// }}} - -namespace Plugin\TagBasedFiltering\Controller; - -use App\Core\Cache; -use App\Core\Controller; -use App\Core\DB\DB; -use App\Core\Form; -use function App\Core\I18n\_m; -use App\Entity\Actor; -use App\Entity\ActorTag; -use App\Entity\ActorTagBlock; -use App\Entity\Language; -use App\Entity\Note; -use App\Entity\NoteTag; -use App\Entity\NoteTagBlock; -use App\Util\Common; -use App\Util\Exception\RedirectException; -use Component\Tag\Tag; -use Functional as F; -use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Core\Type\TextType; -use Symfony\Component\Form\SubmitButton; -use Symfony\Component\HttpFoundation\Request; - -class TagBasedFiltering extends Controller -{ - /** - * Edit the blocked tags of $type_name for target with ID $id. Handles both actor and note tags - */ - private function editBlocked( - Request $request, - ?int $id, - string $type_name, - callable $calculate_target, - callable $calculate_blocks, - callable $calculate_tags, - string $new_label, - string $existing_label, - string $block_class, - ) { - $user = Common::ensureLoggedIn(); - $target = $calculate_target(); - $tag_blocks = $calculate_blocks($user); - $blockable_tags = $calculate_tags($tag_blocks); - - $new_tags_form_definition = []; - foreach ($blockable_tags as $nt) { - $canon = $nt->getCanonical(); - $new_tags_form_definition[] = ["{$canon}:new-tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']]; - $new_tags_form_definition[] = ["{$canon}:use-canon", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]]; - $new_tags_form_definition[] = ["{$canon}:add", SubmitType::class, ['label' => _m('Block')]]; - } - - $existing_tags_form_definition = []; - foreach ($tag_blocks as $ntb) { - $canon = $ntb->getCanonical(); - $existing_tags_form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $ntb->getTag(), 'label' => ' ', 'disabled' => true]]; - $existing_tags_form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['label' => $ntb->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]]; - $existing_tags_form_definition[] = ["{$canon}:remove", SubmitType::class, ['label' => _m('Unblock')]]; - } - - $new_tags_form = null; - if (!empty($new_tags_form_definition) && $user->getId() !== $target->getActorId()) { - $new_tags_form = Form::create($new_tags_form_definition); - $new_tags_form->handleRequest($request); - if ($new_tags_form->isSubmitted() && $new_tags_form->isValid()) { - $data = $new_tags_form->getData(); - foreach ($new_tags_form_definition as [$id, $_, $opts]) { - [$canon, $type] = explode(':', $id); - if ($type === 'add') { - /** @var SubmitButton $button */ - $button = $new_tags_form->get($id); - if ($button->isClicked()) { - Cache::delete($block_class::cacheKey($user->getId())); - Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); - $new_tag = Tag::ensureValid($data[$canon . ':new-tag']); - $language = $target instanceof Note ? Language::getByNote($target)->getLocale() : $user->getActor()->getTopLanguage()->getLocale(); - $canonical_tag = Tag::canonicalTag($new_tag, $language); - DB::persist($block_class::create([ - 'blocker' => $user->getId(), - 'tag' => $new_tag, - 'canonical' => $canonical_tag, - 'use_canonical' => $data[$canon . ':use-canon'], - ])); - DB::flush(); - throw new RedirectException; - } - } - } - } - } - - $existing_tags_form = null; - if (!empty($existing_tags_form_definition)) { - $existing_tags_form = Form::create($existing_tags_form_definition); - $existing_tags_form->handleRequest($request); - if ($existing_tags_form->isSubmitted() && $existing_tags_form->isValid()) { - $data = $existing_tags_form->getData(); - foreach ($existing_tags_form_definition as [$id, $_, $opts]) { - [$canon, $type] = explode(':', $id); - if (\in_array($type, ['remove', 'toggle-canon'])) { - /** @var SubmitButton $button */ - $button = $existing_tags_form->get($id); - if ($button->isClicked()) { - Cache::delete($block_class::cacheKey($user->getId())); - Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); - switch ($type) { - case 'toggle-canon': - $ntb = DB::getReference($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]); - $ntb->setUseCanonical(!$ntb->getUseCanonical()); - DB::flush(); - throw new RedirectException; - case 'remove': - $old_tag = $data[$canon . ':old-tag']; - DB::removeBy($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]); - throw new RedirectException; - } - } - } - } - } - } - - return [ - '_template' => 'tag-based-filtering/edit-tags.html.twig', - $type_name => $target, - 'new_tags_form' => $new_tags_form?->createView(), - 'existing_tags_form' => $existing_tags_form?->createView(), - 'new_label' => $new_label, - 'existing_label' => $existing_label, - ]; - } - - /** - * Edit the user's list of blocked note tags, with the option of adding the notes from the note $note_id, if given - */ - public function editBlockedNoteTags(Request $request, ?int $note_id) - { - return $this->editBlocked( - request: $request, - id: $note_id, - type_name: 'note', - calculate_target: fn () => !\is_null($note_id) ? Note::getById($note_id) : null, - calculate_blocks: fn ($user) => NoteTagBlock::getByActorId($user->getId()), - calculate_tags: fn ($tag_blocks) => F\reject( - !\is_null($note_id) ? NoteTag::getByNoteId($note_id) : [], - fn (NoteTag $nt) => NoteTagBlock::checkBlocksNoteTag($nt, $tag_blocks), - ), - new_label: _m('Tags in the note above:'), - existing_label: _m('Tags you already blocked:'), - block_class: NoteTagBlock::class, - ); - } - - public function editBlockedActorTags(Request $request, ?int $actor_id) - { - return $this->editBlocked( - request: $request, - id: $actor_id, - type_name: 'actor', - calculate_target: fn () => !\is_null($actor_id) ? Actor::getById($actor_id) : null, - calculate_blocks: fn ($user) => ActorTagBlock::getByActorId($user->getId()), - calculate_tags: fn ($tag_blocks) => F\reject( - !\is_null($actor_id) ? ActorTag::getByActorId($actor_id) : [], - fn (ActorTag $nt) => ActorTagBlock::checkBlocksActorTag($nt, $tag_blocks), - ), - new_label: _m('Tags of the account above:'), - existing_label: _m('Tags you already blocked:'), - block_class: ActorTagBlock::class, - ); - } -} diff --git a/plugins/TagBasedFiltering/TagBasedFiltering.php b/plugins/TagBasedFiltering/TagBasedFiltering.php index 673af0bf48..f8788cb09b 100644 --- a/plugins/TagBasedFiltering/TagBasedFiltering.php +++ b/plugins/TagBasedFiltering/TagBasedFiltering.php @@ -38,6 +38,7 @@ use App\Entity\Note; use App\Entity\NoteTag; use App\Entity\NoteTagBlock; use Functional as F; +use Plugin\TagBasedFiltering\Controller as C; use Symfony\Component\HttpFoundation\Request; class TagBasedFiltering extends Plugin @@ -55,8 +56,8 @@ class TagBasedFiltering extends Plugin public function onAddRoute(RouteLoader $r) { - $r->connect(id: self::NOTE_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-note-tags/{note_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedNoteTags']); - $r->connect(id: self::ACTOR_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-actor-tags/{actor_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedActorTags']); + $r->connect(id: self::NOTE_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-note-tags/{note_id<\d+>}', target: [Controller\AddBlocked::class, 'addBlockedNoteTags']); + $r->connect(id: self::ACTOR_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-actor-tags/{actor_id<\d+>}', target: [Controller\AddBlocked::class, 'addBlockedActorTags']); return Event::next; } @@ -105,4 +106,23 @@ class TagBasedFiltering extends Plugin return Event::next; } + + public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool + { + if ($section === 'muting') { + $tabs[] = [ + 'title' => 'Muted note tags', + 'desc' => 'Edit your muted note tags', + 'id' => 'settings-muting-note-tags', + 'controller' => C\EditBlocked::editBlockedNoteTags($request), + ]; + $tabs[] = [ + 'title' => 'Muted people tags', + 'desc' => 'Edit your muted people tags', + 'id' => 'settings-muting-actor-tags', + 'controller' => C\EditBlocked::editBlockedActorTags($request), + ]; + } + return Event::next; + } } diff --git a/plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig b/plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig deleted file mode 100644 index 67ab29d301..0000000000 --- a/plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'base.html.twig' %} -{% import '/cards/note/view.html.twig' as noteView %} - -{% block body %} - {% if note is defined or actor is defined %} -
- {% if note is defined %} - {{ noteView.macro_note(note, {}) }} - {% elseif actor is defined %} - {% include 'cards/profile/view.html.twig' with {'actor': actor} only %} - {% endif %} -
- {% endif %} - {% if new_tags_form is not null %} -

{{ new_label }}

- {{ form(new_tags_form) }} -
- {% endif %} - {% if existing_tags_form is not null %} -

{{ existing_label }}

- {{ form(existing_tags_form) }} - {% endif %} -{% endblock %} diff --git a/plugins/TagBasedFiltering/templates/tag_based_filtering/add_blocked.html.twig b/plugins/TagBasedFiltering/templates/tag_based_filtering/add_blocked.html.twig new file mode 100644 index 0000000000..86b1810abb --- /dev/null +++ b/plugins/TagBasedFiltering/templates/tag_based_filtering/add_blocked.html.twig @@ -0,0 +1,26 @@ +{% extends 'base.html.twig' %} +{% import '/cards/note/view.html.twig' as noteView %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock stylesheets %} + +{% block body %} + {% if note is defined or actor is defined %} +
+ {% if note is defined and note is not null %} + {{ noteView.macro_note(note, {}) }} + {% elseif actor is defined and actor is not null %} + {% include 'cards/profile/view.html.twig' with {'actor': actor} only %} + {% endif %} +
+ {% endif %} + {% if tags_form is not null %} +

{{ label }}

+ {{ form(tags_form) }} + {% endif %} +
+ {% trans %}Go to %type% muting settings{% endtrans %} +
+{% endblock %} diff --git a/plugins/TagBasedFiltering/templates/tag_based_filtering/settings_edit_blocked.html.twig b/plugins/TagBasedFiltering/templates/tag_based_filtering/settings_edit_blocked.html.twig new file mode 100644 index 0000000000..afd6f60142 --- /dev/null +++ b/plugins/TagBasedFiltering/templates/tag_based_filtering/settings_edit_blocked.html.twig @@ -0,0 +1,10 @@ +
+ {% if blocked_form is not null %} + {{ form(blocked_form) }} + {% else %} + {% trans %}No blocked %type% tags{% endtrans %} + {% endif %} +
+

{{ label }}

+ {{ form(add_block) }} +
diff --git a/templates/settings/base.html.twig b/templates/settings/base.html.twig index 63aaf82e59..c8fb51c878 100644 --- a/templates/settings/base.html.twig +++ b/templates/settings/base.html.twig @@ -66,6 +66,11 @@ {{ _self.settings_details_container('Profile', 'Personal Information, Avatar and Profile', 'settings-profile-details', profile_tabs, _context) }}
+
  • + {% set muting_tabs = handle_event('PopulateSettingsTabs', app.request, 'muting') %} + {{ _self.settings_details_container('Muting', 'Blocked tags and actors', 'settings-muting-details', muting_tabs, _context) }} +
  • +
  • {{ _self.settings_details_element('Email', 'Set incoming and outgoing email settings', 'settings-email-details', email_form, _context) }}