diff --git a/Screenshot 2026-08-11 095051.png b/Screenshot 2026-08-11 095051.png new file mode 100644 index 0000000..a48d3fa Binary files /dev/null and b/Screenshot 2026-08-11 095051.png differ diff --git a/config/form.yml b/config/form.yml index d083c1c..5ff8327 100644 --- a/config/form.yml +++ b/config/form.yml @@ -33,6 +33,14 @@ services: arguments: - '@websmid.customhtml.form.customhtmlgroup_data_handler' + websmid.customhtml.form.type.customhtml_faq: + public: true + class: 'Websmid\CustomHtml\Form\Type\CustomHtmlFaqType' + arguments: + - '@translator' + tags: + - { name: form.type } + websmid.customhtml.form.type.customhtml_block: public: true class: 'Websmid\CustomHtml\Form\Type\CustomHtmlBlockType' diff --git a/src/Config/Config.php b/src/Config/Config.php index 736c713..27b5bc0 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -6,6 +6,12 @@ class Config { const HOOKS = [ 'actionAdminControllerSetMedia', + 'actionCustomhtmlblockGridDataModifier', + ]; + + const BLOCK_TYPES = [ + 'html' => 'HTML', + 'faq' => 'FAQ Accordion', ]; const ADMIN_CONTROLLERS = [ diff --git a/src/Controller/CustomHtmlBlockController.php b/src/Controller/CustomHtmlBlockController.php index 7dd0dbe..bf9bec4 100644 --- a/src/Controller/CustomHtmlBlockController.php +++ b/src/Controller/CustomHtmlBlockController.php @@ -11,6 +11,7 @@ use Symfony\Component\HttpFoundation\Response; use Websmid\CustomHtml\Grid\Definition\Factory\CustomHtmlBlockGridDefinitionFactory; use Websmid\CustomHtml\Grid\Filters\CustomHtmlBlockFilters; use Websmid\CustomHtml\Model\CustomHtmlBlock; +use Websmid\CustomHtml\Model\CustomHtmlFaq; use Validate; class CustomHtmlBlockController extends FrameworkBundleAdminController @@ -107,6 +108,7 @@ class CustomHtmlBlockController extends FrameworkBundleAdminController return $this->redirectToRoute('customhtmlblock_index'); } + CustomHtmlFaq::deleteByBlockId($idCustomHtmlBlock); $block->delete(); $this->addFlash('success', $this->trans('Successful deletion.', 'Admin.Notifications.Success')); diff --git a/src/Form/DataHandler/CustomHtmlBlockDataHandler.php b/src/Form/DataHandler/CustomHtmlBlockDataHandler.php index f0f0517..d99f91a 100644 --- a/src/Form/DataHandler/CustomHtmlBlockDataHandler.php +++ b/src/Form/DataHandler/CustomHtmlBlockDataHandler.php @@ -4,6 +4,7 @@ namespace Websmid\CustomHtml\Form\DataHandler; use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataHandler\FormDataHandlerInterface; use Websmid\CustomHtml\Model\CustomHtmlBlock; +use Websmid\CustomHtml\Model\CustomHtmlFaq; use Validate; final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface @@ -13,6 +14,7 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface $block = new CustomHtmlBlock(); $block->id_ws_customhtml_group = (int) $formData['id_ws_customhtml_group']; $block->title = (string) $formData['title']; + $block->type = (string) $formData['type']; $block->show_title = (int) $formData['show_title']; $block->content = (string) $formData['content']; $block->active = (int) $formData['active']; @@ -22,6 +24,8 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface return false; } + $this->saveFaqs((int) $block->id, $formData['faqs'] ?? []); + return $block->id; } @@ -35,10 +39,35 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface $block->id_ws_customhtml_group = (int) $formData['id_ws_customhtml_group']; $block->title = (string) $formData['title']; + $block->type = (string) $formData['type']; $block->show_title = (int) $formData['show_title']; $block->content = (string) $formData['content']; $block->active = (int) $formData['active']; - return (bool) $block->update(); + if (!$block->update()) { + return false; + } + + $this->saveFaqs((int) $id, $formData['faqs'] ?? []); + + return true; + } + + private function saveFaqs(int $idBlock, array $faqs): void + { + CustomHtmlFaq::deleteByBlockId($idBlock); + + foreach (array_values($faqs) as $position => $faqData) { + if (empty($faqData['question'])) { + continue; + } + + $faq = new CustomHtmlFaq(); + $faq->id_ws_customhtml = $idBlock; + $faq->question = (string) $faqData['question']; + $faq->answer = (string) ($faqData['answer'] ?? ''); + $faq->ordering = $position; + $faq->add(); + } } } diff --git a/src/Form/DataProvider/CustomHtmlBlockDataProvider.php b/src/Form/DataProvider/CustomHtmlBlockDataProvider.php index 493e288..4149bbd 100644 --- a/src/Form/DataProvider/CustomHtmlBlockDataProvider.php +++ b/src/Form/DataProvider/CustomHtmlBlockDataProvider.php @@ -4,6 +4,7 @@ namespace Websmid\CustomHtml\Form\DataProvider; use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataProvider\FormDataProviderInterface; use Websmid\CustomHtml\Model\CustomHtmlBlock; +use Websmid\CustomHtml\Model\CustomHtmlFaq; use Validate; final class CustomHtmlBlockDataProvider implements FormDataProviderInterface @@ -19,8 +20,10 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface return [ 'id_ws_customhtml_group' => (int) $block->id_ws_customhtml_group, 'title' => $block->title, + 'type' => $block->type, 'show_title' => (bool) $block->show_title, 'content' => $block->content, + 'faqs' => $this->loadFaqs((int) $id), 'active' => (bool) $block->active, ]; } @@ -30,9 +33,21 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface return [ 'id_ws_customhtml_group' => 0, 'title' => '', + 'type' => 'html', 'show_title' => false, 'content' => '', + 'faqs' => [], 'active' => true, ]; } + + private function loadFaqs(int $idBlock): array + { + return array_map(static function (array $faq) { + return [ + 'question' => $faq['question'], + 'answer' => $faq['answer'], + ]; + }, CustomHtmlFaq::getByBlockId($idBlock)); + } } diff --git a/src/Form/Type/CustomHtmlBlockType.php b/src/Form/Type/CustomHtmlBlockType.php index cf9c45e..945eaf9 100644 --- a/src/Form/Type/CustomHtmlBlockType.php +++ b/src/Form/Type/CustomHtmlBlockType.php @@ -5,11 +5,13 @@ namespace Websmid\CustomHtml\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use PrestaShop\PrestaShop\Core\Form\ConfigurableFormChoiceProviderInterface; use PrestaShopBundle\Form\Admin\Type\SwitchType; use Symfony\Contracts\Translation\TranslatorInterface; +use Websmid\CustomHtml\Config\Config; class CustomHtmlBlockType extends AbstractType { @@ -37,6 +39,11 @@ class CustomHtmlBlockType extends AbstractType 'label' => $this->translator->trans('Title', [], 'Modules.Wscustomhtml.Admin'), 'required' => true, ]) + ->add('type', ChoiceType::class, [ + 'label' => $this->translator->trans('Block type', [], 'Modules.Wscustomhtml.Admin'), + 'choices' => array_flip(Config::BLOCK_TYPES), + 'required' => true, + ]) ->add('show_title', SwitchType::class, [ 'label' => $this->translator->trans('Show title', [], 'Modules.Wscustomhtml.Admin'), 'required' => false, @@ -45,6 +52,15 @@ class CustomHtmlBlockType extends AbstractType 'label' => $this->translator->trans('Content', [], 'Modules.Wscustomhtml.Admin'), 'required' => false, 'attr' => ['class' => 'autoload_rte'], + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-html'], + ]) + ->add('faqs', CollectionType::class, [ + 'entry_type' => CustomHtmlFaqType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'prototype' => true, + 'required' => false, ]) ->add('active', SwitchType::class, [ 'label' => $this->translator->trans('Active', [], 'Modules.Wscustomhtml.Admin'), diff --git a/src/Form/Type/CustomHtmlFaqType.php b/src/Form/Type/CustomHtmlFaqType.php new file mode 100644 index 0000000..bd952f9 --- /dev/null +++ b/src/Form/Type/CustomHtmlFaqType.php @@ -0,0 +1,32 @@ +translator = $translator; + } + + public function buildForm(FormBuilderInterface $formBuilder, array $options): void + { + $formBuilder + ->add('question', TextType::class, [ + 'label' => $this->translator->trans('Question', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + ]) + ->add('answer', TextareaType::class, [ + 'label' => $this->translator->trans('Answer', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + ]); + } +} diff --git a/src/Grid/Definition/Factory/CustomHtmlBlockGridDefinitionFactory.php b/src/Grid/Definition/Factory/CustomHtmlBlockGridDefinitionFactory.php index 8c168dd..33ccb58 100644 --- a/src/Grid/Definition/Factory/CustomHtmlBlockGridDefinitionFactory.php +++ b/src/Grid/Definition/Factory/CustomHtmlBlockGridDefinitionFactory.php @@ -15,7 +15,9 @@ use PrestaShop\PrestaShop\Core\Grid\Filter\Filter; use PrestaShop\PrestaShop\Core\Grid\Filter\FilterCollection; use PrestaShop\PrestaShop\Core\Hook\HookDispatcherInterface; use PrestaShopBundle\Form\Admin\Type\SearchAndResetType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Websmid\CustomHtml\Config\Config; final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionFactory { @@ -71,6 +73,11 @@ final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionF ->setName($this->trans('Group', [], 'Modules.Wscustomhtml.Admin')) ->setOptions(['field' => 'group_title']) ) + ->add( + (new DataColumn('type')) + ->setName($this->trans('Type', [], 'Modules.Wscustomhtml.Admin')) + ->setOptions(['field' => 'type']) + ) ->add( (new DataColumn('active')) ->setName($this->trans('Active', [], 'Admin.Global')) @@ -120,6 +127,15 @@ final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionF 'attr' => ['placeholder' => $this->trans('Group', [], 'Modules.Wscustomhtml.Admin')], ]) ) + ->add( + (new Filter('type', ChoiceType::class)) + ->setAssociatedColumn('type') + ->setTypeOptions([ + 'required' => false, + 'choices' => array_flip(Config::BLOCK_TYPES), + 'placeholder' => $this->trans('All', [], 'Admin.Global'), + ]) + ) ->add( (new Filter('actions', SearchAndResetType::class)) ->setAssociatedColumn('actions') diff --git a/src/Grid/Query/CustomHtmlBlockQueryBuilder.php b/src/Grid/Query/CustomHtmlBlockQueryBuilder.php index 0c7fad2..ad20f83 100644 --- a/src/Grid/Query/CustomHtmlBlockQueryBuilder.php +++ b/src/Grid/Query/CustomHtmlBlockQueryBuilder.php @@ -45,6 +45,7 @@ final class CustomHtmlBlockQueryBuilder extends AbstractDoctrineQueryBuilder 'wchb.`id_ws_customhtml`', 'wchb.`title`', 'wchg.`title` AS group_title', + 'wchb.`type`', 'wchb.`active`', 'wchb.`ordering`', 'wchb.`date_add`' @@ -108,6 +109,12 @@ final class CustomHtmlBlockQueryBuilder extends AbstractDoctrineQueryBuilder continue; } + if ($filterName === 'type') { + $qb->andWhere('wchb.`type` = :type') + ->setParameter('type', pSQL($filterValue)); + continue; + } + $qb->andWhere('wchb.`' . $filterName . '` LIKE :' . $filterName) ->setParameter($filterName, '%' . pSQL($filterValue) . '%'); } diff --git a/src/Model/CustomHtmlBlock.php b/src/Model/CustomHtmlBlock.php index e485c60..87ac2d3 100644 --- a/src/Model/CustomHtmlBlock.php +++ b/src/Model/CustomHtmlBlock.php @@ -16,6 +16,9 @@ class CustomHtmlBlock extends ObjectModel /** @var string */ public $title = ''; + /** @var string */ + public $type = 'html'; + /** @var string */ public $content = ''; @@ -40,6 +43,7 @@ class CustomHtmlBlock extends ObjectModel 'fields' => [ 'id_ws_customhtml_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 'title' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 256], + 'type' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 32], 'content' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'], 'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'show_title' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], @@ -58,7 +62,7 @@ class CustomHtmlBlock extends ObjectModel } $result = Db::getInstance()->executeS( - 'SELECT `id_ws_customhtml`, `id_ws_customhtml_group`, `title`, `content`, `active`, `show_title`, `ordering` + 'SELECT `id_ws_customhtml`, `id_ws_customhtml_group`, `title`, `type`, `content`, `active`, `show_title`, `ordering` FROM `' . _DB_PREFIX_ . 'ws_customhtml` WHERE `id_ws_customhtml_group` IN (' . implode(',', $groupIds) . ') AND `active` = 1 diff --git a/src/Model/CustomHtmlFaq.php b/src/Model/CustomHtmlFaq.php new file mode 100644 index 0000000..fd4a30b --- /dev/null +++ b/src/Model/CustomHtmlFaq.php @@ -0,0 +1,60 @@ + 'ws_customhtml_faq', + 'primary' => 'id_ws_customhtml_faq', + 'fields' => [ + 'id_ws_customhtml' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], + 'question' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 512], + 'answer' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'], + 'ordering' => ['type' => self::TYPE_INT, 'validate' => 'isInt'], + 'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], + 'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], + ], + ]; + + public static function getByBlockId(int $idBlock): array + { + $result = Db::getInstance()->executeS( + 'SELECT `id_ws_customhtml_faq`, `question`, `answer`, `ordering` + FROM `' . _DB_PREFIX_ . 'ws_customhtml_faq` + WHERE `id_ws_customhtml` = ' . (int) $idBlock . ' + ORDER BY `ordering` ASC' + ); + + return $result ?: []; + } + + public static function deleteByBlockId(int $idBlock): void + { + Db::getInstance()->delete('ws_customhtml_faq', '`id_ws_customhtml` = ' . (int) $idBlock); + } +} diff --git a/src/Module/AdminHooks.php b/src/Module/AdminHooks.php index 0d05f41..e691fba 100644 --- a/src/Module/AdminHooks.php +++ b/src/Module/AdminHooks.php @@ -3,6 +3,8 @@ namespace Websmid\CustomHtml\Module; use Websmid\CustomHtml\Config\Config; +use PrestaShop\PrestaShop\Core\Grid\Data\GridData; +use PrestaShop\PrestaShop\Core\Grid\Record\RecordCollection; use Tools; trait AdminHooks @@ -27,4 +29,17 @@ trait AdminHooks $this->context->controller->addCSS($this->getPathUri() . 'views/css/admin/grid.css'); } + + public function hookActionCustomhtmlblockGridDataModifier(array $params): void + { + $gridData = $params['data']; + + $records = array_map(static function (array $row) { + $row['type'] = Config::BLOCK_TYPES[$row['type']] ?? $row['type']; + + return $row; + }, $gridData->getRecords()->all()); + + $params['data'] = new GridData(new RecordCollection($records), $gridData->getRecordsTotal(), $gridData->getQuery()); + } } diff --git a/src/Module/DisplayHooks.php b/src/Module/DisplayHooks.php index 7dc4865..09e3e9a 100644 --- a/src/Module/DisplayHooks.php +++ b/src/Module/DisplayHooks.php @@ -3,6 +3,7 @@ namespace Websmid\CustomHtml\Module; use Websmid\CustomHtml\Model\CustomHtmlBlock; +use Websmid\CustomHtml\Model\CustomHtmlFaq; use Websmid\CustomHtml\Model\CustomHtmlGroup; use Context; @@ -54,6 +55,7 @@ trait DisplayHooks } $blocksByGroup = CustomHtmlBlock::getActiveByGroupIds(array_column($groups, 'id_ws_customhtml_group')); + $blocksByGroup = $this->attachFaqsToBlocks($blocksByGroup); $groups = array_map(static function (array $group) use ($blocksByGroup) { $group['blocks'] = $blocksByGroup[(int) $group['id_ws_customhtml_group']] ?? []; @@ -65,4 +67,17 @@ trait DisplayHooks return !empty($group['blocks']); })); } + + private function attachFaqsToBlocks(array $blocksByGroup): array + { + return array_map(static function (array $blocks) { + return array_map(static function (array $block) { + if ($block['type'] === 'faq') { + $block['faqs'] = CustomHtmlFaq::getByBlockId((int) $block['id_ws_customhtml']); + } + + return $block; + }, $blocks); + }, $blocksByGroup); + } } diff --git a/src/Module/Install.php b/src/Module/Install.php index 5ad1cda..85fc1a1 100644 --- a/src/Module/Install.php +++ b/src/Module/Install.php @@ -3,7 +3,9 @@ namespace Websmid\CustomHtml\Module; use Websmid\CustomHtml\Config\Config; +use Context; use Db; +use Hook; use Language; use Tab; @@ -31,6 +33,11 @@ trait Install return false; } + if (!$this->registerGroupHooks()) { + error_log('[ws_customhtml] registerGroupHooks() failed'); + return false; + } + return true; } @@ -55,6 +62,7 @@ trait Install `id_ws_customhtml` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `id_ws_customhtml_group` INT(11) NOT NULL, `title` VARCHAR(256) NOT NULL DEFAULT \'\', + `type` VARCHAR(32) NOT NULL DEFAULT \'html\', `content` LONGTEXT, `active` TINYINT(1) NOT NULL DEFAULT 1, `show_title` TINYINT(1) NOT NULL DEFAULT 0, @@ -65,6 +73,19 @@ trait Install ); '; + $sqlToExec[] = ' + CREATE TABLE IF NOT EXISTS ' . _DB_PREFIX_ . 'ws_customhtml_faq ( + `id_ws_customhtml_faq` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `id_ws_customhtml` INT(11) NOT NULL, + `question` VARCHAR(512) NOT NULL DEFAULT \'\', + `answer` TEXT, + `ordering` INT(11) NOT NULL DEFAULT 0, + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + KEY `id_ws_customhtml` (`id_ws_customhtml`) + ); + '; + foreach ($sqlToExec as $sqlBlock) { Db::getInstance()->execute(trim($sqlBlock)); } @@ -83,6 +104,29 @@ trait Install return true; } + private function registerGroupHooks(): bool + { + $hookNames = Db::getInstance()->executeS( + 'SELECT DISTINCT `hook_name` FROM `' . _DB_PREFIX_ . 'ws_customhtml_group`' + ); + + $idShop = (int) Context::getContext()->shop->id; + + foreach ($hookNames as $row) { + $hookName = $row['hook_name']; + + if (Hook::isModuleRegisteredOnHook($this, $hookName, $idShop)) { + continue; + } + + if (!$this->registerHook($hookName)) { + return false; + } + } + + return true; + } + private function registerTabs(array $moduleTabs): bool { $languages = Language::getLanguages(false); diff --git a/upgrade/upgrade-1.2.0.php b/upgrade/upgrade-1.2.0.php new file mode 100644 index 0000000..8940450 --- /dev/null +++ b/upgrade/upgrade-1.2.0.php @@ -0,0 +1,38 @@ +executeS( + 'SHOW COLUMNS FROM `' . _DB_PREFIX_ . 'ws_customhtml` LIKE \'type\'' + ); + + if (empty($columns)) { + if (!Db::getInstance()->execute( + 'ALTER TABLE `' . _DB_PREFIX_ . 'ws_customhtml` + ADD COLUMN `type` VARCHAR(32) NOT NULL DEFAULT \'html\' AFTER `title`' + )) { + return false; + } + } + + if (!Db::getInstance()->execute( + 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customhtml_faq` ( + `id_ws_customhtml_faq` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `id_ws_customhtml` INT(11) NOT NULL, + `question` VARCHAR(512) NOT NULL DEFAULT \'\', + `answer` TEXT, + `ordering` INT(11) NOT NULL DEFAULT 0, + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + KEY `id_ws_customhtml` (`id_ws_customhtml`) + )' + )) { + return false; + } + + return $module->registerHook('actionCustomhtmlblockGridDataModifier'); +} diff --git a/views/templates/admin/form/blocks/block_form.html.twig b/views/templates/admin/form/blocks/block_form.html.twig index 01e4c2d..3267dc6 100644 --- a/views/templates/admin/form/blocks/block_form.html.twig +++ b/views/templates/admin/form/blocks/block_form.html.twig @@ -1,6 +1,12 @@ {% import '@PrestaShop/Admin/macros.html.twig' as ps %} {% form_theme customHtmlBlockForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %} +{% macro faqFields(faqForm) %} + {{ form_row(faqForm.question) }} + {{ form_row(faqForm.answer) }} +{% endmacro %} +{% import _self as faqMacros %} + {% block customhtmlblock_form_block %}