ソースを参照

#3393 Help Tours module initial commit.

Alexander 5 年 前
親
コミット
5bf151e147

+ 20 - 0
modules/boonex/help_tours/classes/BxHelpToursConfig.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursConfig extends BxBaseModGeneralConfig
+{
+    public function __construct($aModule)
+    {
+        parent::__construct($aModule);
+    }
+}
+
+/** @} */

+ 150 - 0
modules/boonex/help_tours/classes/BxHelpToursDb.php

@@ -0,0 +1,150 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursDb extends BxDolModuleDb
+{
+    private $_sTourBlockTemplate = 'a:3:{s:6:"module";s:13:"bx_help_tours";s:6:"method";s:13:"get_help_tour";s:6:"params";a:1:{i:0;i:__tour_id__;}}';
+    private $_sBlockSelectorTemplate = '#bx-page-block-__block_id__ > div';
+
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function getTourDetails($iTourId) {
+        return $this->getRow("SELECT * FROM `bx_help_tours` WHERE `id` = :id", ['id' => $iTourId]);
+    }
+
+    public function getTourPageUrl($iTourId) {
+        $aTour = $this->getTourDetails($iTourId);
+        if (!$aTour) return false;
+
+        if ($aTour['page'] == 'sys_home') return '/';
+
+        return $this->getOne("SELECT `url` FROM `sys_objects_page` WHERE `object` = :page", ['page' => $aTour['page']]);
+    }
+
+    public function getSitePages() {
+        $aPages = $this->getAll("SELECT `object`, `module`, `title_system` AS `name`  FROM `sys_objects_page` WHERE `title_system` <> '' ORDER BY `title_system` ASC");
+        $aPagesByModules = [];
+        foreach ($aPages as $aPage) {
+            $aPagesByModules[$aPage['module']][$aPage['object']] = _t($aPage['name']);
+        }
+
+        $aResult = [];
+        foreach ($aPagesByModules as $sModule => $aPages) {
+            $oModule = BxDolModule::getInstance($sModule);
+            $sTitle = $oModule && isset($oModule->_aModule) ? $oModule->_aModule['title'] : ucfirst($sModule);
+
+            $aResult[$sModule.'_start'] = ['type' => 'group_header', 'value' => $sTitle];
+            $aResult = $aResult + $aPages;
+            $aResult[$sModule.'_end'] = ['type' => 'group_end'];
+        }
+
+        return $aResult;
+    }
+
+    public function deleteHelpTourItems($iEntry) {
+        $this->query("DELETE FROM `bx_help_tours_items` WHERE `tour` = :tour", ['tour' => $iEntry]);
+    }
+
+    public function getPageTitleDetails($sPageObject) {
+        $aData = $this->getRow("SELECT `module`, `title_system` FROM `sys_objects_page` WHERE `object` = :page", ['page' => $sPageObject]);
+        if (!$aData) return false;
+
+        $oModule = BxDolModule::getInstance($aData['module']);
+        $aData['module_title'] = $oModule && isset($oModule->_aModule) ? $oModule->_aModule['title'] : ucfirst($aData['module']);
+
+        return $aData;
+    }
+
+    public function putHelpTourOnPage($iTourId, $sOldPage) {
+        $aTour = $this->getTourDetails($iTourId);
+        $sPage = $aTour['page'];
+
+        if ($sOldPage != $sPage) {
+            if (!empty($sOldPage)) {
+                $this->query("DELETE FROM `sys_pages_blocks` WHERE `object` = :page AND `module` = 'bx_help_tours'", [
+                    'page' => $sOldPage,
+                ]);
+            }
+
+            $iOrder = intval($this->getOne("SELECT MAX(`order`) FROM `sys_pages_blocks` WHERE `object` = :page and `cell_id` = 1", ['page' => $sPage])) + 1;
+
+            $this->query("
+                INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES
+                (:page, 1, 'bx_help_tours', '_bx_help_tours_page_block_caption', 0, 2147483647, 'service', :service_call, 0, 0, :order);
+            ", [
+                'page' => $sPage,
+                'service_call' => str_replace('__tour_id__', $iTourId, $this->_sTourBlockTemplate),
+                'order' => $iOrder,
+            ]);
+        }
+    }
+
+    function deleteHelpTourBlock($iTourId) {
+        $aTour = $this->getTourDetails($iTourId);
+
+        $this->query("DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_help_tours' AND `object` = :page", [
+            'page' => $aTour['page'],
+        ]);
+    }
+
+    function getHelpTourItemsCount($iTourId) {
+        return $this->getOne("SELECT COUNT(*) FROM `bx_help_tours_items` WHERE `tour` = :tour", [
+            'tour' => $iTourId,
+        ]);
+    }
+
+    public function getHelpTourItems($iTourId) {
+        return $this->getAll("SELECT * FROM `bx_help_tours_items` WHERE `tour` = :tour ORDER BY `order` ASC", [
+            'tour' => $iTourId,
+        ]);
+    }
+
+    public function getTourItemDetails($iItemId) {
+        return $this->getRow("SELECT * FROM `bx_help_tours_items` WHERE `id` = :id", ['id' => $iItemId]);
+    }
+
+    public function getPageBlocksForHelpTour($iTourId) {
+        $aTour = $this->getTourDetails($iTourId);
+        if (!$aTour) return [];
+
+        $aResult = [];
+
+        $aRes = $this->getAll("SELECT `id`, `title_system`, `title` FROM `sys_pages_blocks` WHERE `object` = :page AND `cell_id` > 0 AND `active` AND `module` <> 'bx_help_tours' AND (`title_system` <> '' OR `title` <> '') ORDER BY `cell_id` ASC, `order` ASC", ['page' => $aTour['page']]);
+        if ($aRes) {
+            foreach ($aRes as $aRow) {
+                $aResult[str_replace('__block_id__', $aRow['id'], $this->_sBlockSelectorTemplate)] = !empty($aRow['title_system']) ? _t($aRow['title_system']) : _t($aRow['title']);
+            }
+        }
+
+        return $aResult;
+    }
+
+    public function getHelpToursOptions() {
+        return $this->getPairs("SELECT `id`, `page` FROM `bx_help_tours` ORDER BY `order` ASC", 'id', 'page');
+    }
+
+    public function deleteAccountData($iAccount) {
+        $this->query("DELETE FROM `bx_help_tours_track_views` WHERE `account` = :account", ['account' => $iAccount]);
+    }
+
+    public function trackTourSeen($iAccount, $iTour) {
+        $this->query("INSERT IGNORE INTO `bx_help_tours_track_views` (`account`, `tour`) VALUES(:account, :tour)", ['account' => $iAccount, 'tour' => $iTour]);
+    }
+
+    public function isHelpTourSeen($iAccount, $iTour) {
+        return $this->getOne("SELECT COUNT(*) FROM `bx_help_tours_track_views` WHERE `account` = :account AND `tour` = :tour", ['account' => $iAccount, 'tour' => $iTour]);
+    }
+}
+
+/** @} */

+ 369 - 0
modules/boonex/help_tours/classes/BxHelpToursGridItems.php

@@ -0,0 +1,369 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursGridItems extends BxTemplGrid
+{
+    protected $_iTourId;
+    protected $_aTourPageBlocks;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->MODULE = 'bx_help_tours';
+    	$this->_oModule = BxDolModule::getInstance($this->MODULE);
+    	if(!$oTemplate)
+			$oTemplate = $this->_oModule->_oTemplate;
+
+    	parent::__construct ($aOptions, $oTemplate);
+
+    	$this->_iTourId = bx_get('tour');
+        if ($this->_iTourId) {
+            $this->_aQueryAppend['tour'] = $this->_iTourId;
+            $this->_aTourPageBlocks = $this->_oModule->_oDb->getPageBlocksForHelpTour($this->_iTourId);
+        }
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        if(!empty($this->_iTourId)) {
+            $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(" AND `tour` = ? ", $this->_iTourId);
+            return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+        }
+        return false;
+    }
+
+    protected function _getCellElement($mixedValue, $sKey, $aField, $aRow)
+    {
+        if (isset($this->_aTourPageBlocks[$aRow['element']])) $mixedValue = htmlspecialchars($this->_aTourPageBlocks[$aRow['element']]);
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    private function getAddEditForm($iEntry) {
+        $aEntry = false;
+
+        if ($iEntry) {
+            $aEntry = $this->_oModule->_oDb->getTourItemDetails($iEntry);
+            if (!$aEntry) {
+                echoJson([]);
+                exit;
+            }
+        }
+
+        $oForm = $this->getAddEditFormObject($iEntry);
+
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT.'grid.php?o=' . $this->_sObject . '&a='.($aEntry ? 'edit' : 'add');
+
+        if ($aEntry) {
+            $aEntry['element_helper'] = $aEntry['element'];
+            if ($aEntry['element'] && !isset($this->_aTourPageBlocks[$aEntry['element']])) $aEntry['element_helper'] = 'custom';
+        }
+        $oForm->initChecker($aEntry);
+        if (isset($oForm->aInputs['element_helper']['value']) && $oForm->aInputs['element_helper']['value'] == 'custom')
+            unset($oForm->aInputs['element']['tr_attrs']);
+
+        if ($oForm->isSubmittedAndValid()) {
+            if ($aEntry) {
+                $oForm->update($aEntry['id']);
+                $iResult = $aEntry['id'];
+            } else {
+                $iResult = $oForm->insert();
+            }
+
+            if ($iResult) {
+                $aRes = ['grid' => $this->getCode(false), 'blink' => $iResult];
+            } else {
+                $aRes = ['msg' => "Error occured"]; // if record adding failed, display error message
+            }
+            echoJson($aRes);
+        } else {
+            $s = '
+                <div class="bx-def-color-bg-block">' . $oForm->getCode(true) . '</div>                
+                <script>                    
+                    $(document).ready(function () {
+                        $("#'.$oForm->aFormAttrs['id'].'").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->aFormAttrs['id'] . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-active:visible").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "'.($aEntry ? 'edit' : 'add').'");
+                            }
+                        });
+                    });
+                </script>';
+
+            echoJson(['popup' => [
+                'html' => BxTemplStudioFunctions::getInstance()->popupBox('bx-tour-item-details-popup', _t('_bx_help_tours_cpt_tour_item_details'), $s),
+                'options' => [
+                    'onBeforeShow' => empty($aEntry) ? "$('#grid-popup-bx_help_tours_items-edit').remove();" : "$('#grid-popup-bx_help_tours_items-add').remove();",
+                    'closeOnOuterClick' => false,
+                ]]]
+            );
+        }
+    }
+
+    public function performActionAdd()
+    {
+        if(!$this->canAdd()) {
+            echoJson([]);
+            exit;
+        }
+
+        return $this->getAddEditForm(0);
+    }
+
+    public function performActionEdit()
+    {
+        $iEntry = !is_null($_REQUEST) && isset($_REQUEST['id']) ? intval($_REQUEST['id']) : intval($_REQUEST['ids'][0]);
+        if(!$iEntry) {
+            echoJson([]);
+            exit;
+        }
+
+        return $this->getAddEditForm($iEntry);
+    }
+
+    private function getAddEditFormObject($iEntry) {
+        $aArrowOptionsSrc = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'];
+        $aArrowOptions = ['' => _t('_bx_help_tours_arrow_option_none')];
+        foreach ($aArrowOptionsSrc as $sOption) {
+            $aArrowOptions[$sOption] = $sOption;
+        }
+
+        $aForm = [
+            'form_attrs' => [
+                'method' => 'post',
+                'action' => '',
+                'id' => 'bx-help-tour-item-details-form',
+            ],
+            'params' => [
+                'db' => [
+                    'submit_name' => 'submit',
+                    'table' => 'bx_help_tours_items',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                ],
+            ],
+            'inputs' => [
+                'id' => [
+                    'type' => 'hidden',
+                    'name' => 'id',
+                    'value' => $iEntry,
+                ],
+                'tour' => [
+                    'type' => 'hidden',
+                    'name' => 'tour',
+                    'value' => $this->_iTourId,
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'name' => [
+                    'type' => 'text',
+                    'name' => 'name',
+                    'required' => true,
+                    'caption' => _t('_bx_help_tours_form_field_name'),
+                    'info' => _t('_bx_help_tours_form_field_name_info'),
+                    'checker' => [
+                        'func' => 'Avail',
+                        'error' => _t('_bx_help_tours_form_field_name_error'),
+                    ],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'element_helper' => [
+                    'type' => 'select',
+                    'name' => 'element_helper',
+                    'caption' => _t('_bx_help_tours_form_field_element'),
+                    'info' => _t('_bx_help_tours_form_field_element_info'),
+                    'values' =>
+                        ['' => _t('_bx_help_tours_form_field_option_none')] +
+                        $this->_aTourPageBlocks +
+                        ['custom' => _t('_bx_help_tours_form_field_option_custom')],
+                    'attrs' => [
+                        'onchange' => "
+                            $('#bx-help-tours-input-element').val(this.value == 'custom' ? '' : this.value);
+                            if (this.value == 'custom') $('#bx-form-element-element').fadeIn(); else $('#bx-form-element-element').fadeOut();                            
+                         ",
+                    ],
+                ],
+                'element' => [
+                    'type' => 'text',
+                    'name' => 'element',
+                    'caption' => _t('_bx_help_tours_form_field_element_custom'),
+                    'info' => _t('_bx_help_tours_form_field_element_custom_info'),
+                    'attrs' => [
+                        'id' => 'bx-help-tours-input-element',
+                    ],
+                    'tr_attrs' => [
+                        'style' => 'display:none;',
+                    ],
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ],
+                'title' => [
+                    'type' => 'text_translatable',
+                    'name' => 'title',
+                    'caption' => _t('_bx_help_tours_form_field_title'),
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'text' => [
+                    'type' => 'textarea_translatable',
+                    'name' => 'text',
+                    'required' => true,
+                    'caption' => _t('_bx_help_tours_form_field_text'),
+                    'checker' => [
+                        'func' => 'AvailTranslatable',
+                        'params' => ['text'],
+                        'error' => _t('_bx_help_tours_form_field_text_error'),
+                    ],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                    'code' => true, //to avoid loosing HTML formatting
+                ],
+                'arrow' => [
+                    'type' => 'select',
+                    'name' => 'arrow',
+                    'caption' => _t('_bx_help_tours_form_field_arrow'),
+                    'values' => $aArrowOptions,
+                    'value' => 'auto',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ],
+                'actions' => [
+                    'type' => 'input_set',
+                    0 => [
+                        'type' => 'submit',
+                        'name' => 'submit',
+                        'value' => _t('_Submit'),
+                    ],
+                    1 => [
+                        'type' => 'button',
+                        'name' => 'cancel',
+                        'value' => _t('_Cancel'),
+                        'attrs' => [
+                            'style' => 'margin-left: 20px;',
+                            'onclick' => '$(".bx-popup-active:visible").dolPopupHide();',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        return new BxTemplStudioFormView($aForm);
+    }
+
+    public function performActionDelete() {
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $oLanguage = BxDolStudioLanguagesUtils::getInstance();
+
+        foreach ($aIds as $iEntry) {
+            $aItem = $this->_oModule->_oDb->getTourItemDetails($iEntry);
+            $oLanguage->deleteLanguageString($aItem['title']);
+            $oLanguage->deleteLanguageString($aItem['text']);
+            $this->_delete($iEntry);
+        }
+
+        echoJson(array(
+            'grid' => $this->getCode(false),
+        ));
+    }
+
+    public function _getActionPreview($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array()) {
+        $sUrl = $this->_oModule->_oDb->getTourPageUrl($this->_iTourId);
+        if (!$sUrl) return '';
+        $sUrl = trim($sUrl, '/');
+
+        $a['attr']['href'] = BX_DOL_URL_ROOT.bx_append_url_params(BxDolPermalinks::getInstance()->permalink($sUrl), ['help_tour_preview' => $this->_iTourId, 'help_tour_item' => $aRow['id']]);
+        $a['attr']['target'] = '_blank';
+
+        $sButton = $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+        $sA = str_replace('button', 'a', $sButton);
+        return $sA;
+    }
+
+    protected function canAdd()
+    {
+        return !empty($this->_iTourId);
+    }
+
+    protected function _getActionAdd($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if(!$this->canAdd())
+            $isDisabled = true;
+
+        return parent::_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getFilterControls ()
+    {
+        $aTourOptions = $this->_oModule->_oDb->getHelpToursOptions();
+        if ($aTourOptions) foreach ($aTourOptions as $iTour => $sPage) {
+            $aPageTitle = $this->_oModule->_oDb->getPageTitleDetails($sPage);
+            if ($aPageTitle)
+                $aTourOptions[$iTour] = $aPageTitle['module_title'].': '._t($aPageTitle['title_system']);
+        }
+
+        $aInputTour = [
+            'type' => 'select',
+            'name' => 'tour',
+            'value' => $this->_iTourId,
+            'values' => ['' => _t('_sys_please_select')] + $aTourOptions,
+            'attrs' => array(
+                'id' => 'bx-grid-tour',
+                'onChange' => 'javascript:' . $this->getJsObject() . '.onChangeTour(this.value);',
+            ),
+        ];
+
+        $oForm = new BxTemplStudioFormView(array());
+        return $oForm->genRow($aInputTour).parent::_getFilterControls();
+    }
+
+    protected function _addJsCss()
+    {
+        parent::_addJsCss();
+        $this->_oModule->_oTemplate->addStudioCss('admin.css');
+        $this->_oModule->_oTemplate->addStudioJs('grid_items.js');
+
+        $oForm = new BxTemplStudioFormView(array());
+        $oForm->addCssJs();
+    }
+
+    function getJsObject()
+    {
+        return 'oBxHelpToursGridItems';
+    }
+
+    function getCode($isDisplayHeader = true)
+    {
+        return $this->_oModule->_oTemplate->parseHtmlByName('grid_items.html', [
+            'module_url' => BX_DOL_URL_ROOT.$this->_oModule->_oConfig->getBaseUri(),
+            'content' => parent::getCode($isDisplayHeader),
+            'js_object' => $this->getJsObject(),
+            'grid_object' => $this->_sObject,
+            'tour' => $this->_iTourId,
+            'page_url' => BX_DOL_URL_STUDIO.'module.php?name='.$this->MODULE.'&page=items',
+        ]);
+    }
+}
+
+/** @} */

+ 237 - 0
modules/boonex/help_tours/classes/BxHelpToursGridTours.php

@@ -0,0 +1,237 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursGridTours extends BxTemplGrid
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->MODULE = 'bx_help_tours';
+    	$this->_oModule = BxDolModule::getInstance($this->MODULE);
+    	if(!$oTemplate)
+			$oTemplate = $this->_oModule->_oTemplate;
+
+        parent::__construct ($aOptions, $oTemplate);
+    }
+    protected function _getCellPage($mixedValue, $sKey, $aField, $aRow)
+    {
+        $aPageTitle = $this->_oModule->_oDb->getPageTitleDetails($mixedValue);
+        if ($aPageTitle)
+            $mixedValue = $aPageTitle['module_title'].': '._t($aPageTitle['title_system']);
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellItems($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sLink = 'javascript: glGrids.'.$this->_sObject.'.action(\'edit_items\', {id: '.$aRow['id'].'});';
+        $mixedValue = $this->_oTemplate->parseLink($sLink, _t('_bx_help_tours_cpt_items_cnt', $this->_oModule->_oDb->getHelpTourItemsCount($aRow['id'])));
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    public function _getActionPreview($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array()) {
+        $sUrl = $this->_oModule->_oDb->getTourPageUrl($aRow['id']);
+        if (!$sUrl) return '';
+        $sUrl = trim($sUrl, '/');
+
+        $a['attr']['href'] = BX_DOL_URL_ROOT.bx_append_url_params(BxDolPermalinks::getInstance()->permalink($sUrl), ['help_tour_preview' => $aRow['id']]);
+        $a['attr']['target'] = '_blank';
+
+        $sButton = $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+        $sA = str_replace('button', 'a', $sButton);
+        return $sA;
+    }
+
+    private function getAddEditForm($iEntry) {
+        $aEntry = false;
+
+        if ($iEntry) {
+            $aEntry = $this->_oModule->_oDb->getTourDetails($iEntry);
+            if (!$aEntry) {
+                echoJson([]);
+                exit;
+            }
+        }
+
+        $oForm = $this->getAddEditFormObject($iEntry);
+
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT.'grid.php?o=' . $this->_sObject . '&a='.($aEntry ? 'edit' : 'add');
+
+        $oForm->initChecker($aEntry);
+
+        if ($oForm->isSubmittedAndValid()) {
+            if ($aEntry) {
+                $oForm->update($aEntry['id']);
+                $iResult = $aEntry['id'];
+            } else {
+                $iResult = $oForm->insert();
+            }
+
+            if ($iResult) {
+                $this->_oModule->_oDb->putHelpTourOnPage($iResult, $aEntry ? $aEntry['page'] : '');
+                $aRes = ['grid' => $this->getCode(false), 'blink' => $iResult];
+            } else {
+                $aRes = ['msg' => "Error occured"]; // if record adding failed, display error message
+            }
+            echoJson($aRes);
+        } else {
+            $s = '
+                <div class="bx-def-color-bg-block">' . $oForm->getCode(true) . '</div>                
+                <script>                    
+                    $(document).ready(function () {
+                        $("#'.$oForm->aFormAttrs['id'].'").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->aFormAttrs['id'] . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-active:visible").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "'.($aEntry ? 'edit' : 'add').'");
+                            }
+                        });
+                    });
+                </script>';
+
+            echoJson(['popup' => [
+                'html' => BxTemplStudioFunctions::getInstance()->popupBox('bx-tour-details-popup', _t('_bx_help_tours_cpt_tour_details'), $s),
+                'options' => [
+                    'closeOnOuterClick' => false,
+                ]]]
+            );
+        }
+    }
+
+    public function performActionAdd()
+    {
+        return $this->getAddEditForm(0);
+    }
+
+    public function performActionEdit()
+    {
+        $iEntry = !is_null($_REQUEST) && isset($_REQUEST['id']) ? intval($_REQUEST['id']) : intval($_REQUEST['ids'][0]);
+        if(!$iEntry) {
+            echoJson([]);
+            exit;
+        }
+
+        return $this->getAddEditForm($iEntry);
+    }
+
+    public function performActionEditItems()
+    {
+        $iTourId = !is_null($_REQUEST) && isset($_REQUEST['id']) ? intval($_REQUEST['id']) : intval($_REQUEST['ids'][0]);
+
+        if (!$iTourId) {
+            echoJson(array());
+            exit;
+        }
+
+        echoJson(array(
+            'redirect' => BX_DOL_URL_STUDIO.'module.php?name='.$this->MODULE.'&tour='.$iTourId.'&page=items',
+        ));
+    }
+
+    public function performActionDelete() {
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $oLanguage = BxDolStudioLanguagesUtils::getInstance();
+
+        foreach ($aIds as $iEntry) {
+            $this->_oModule->_oDb->deleteHelpTourBlock($iEntry);
+
+            $aItems = $this->_oModule->_oDb->getHelpTourItems($iEntry);
+            if ($aItems) foreach ($aItems as $aItem) {
+                $oLanguage->deleteLanguageString($aItem['title']);
+                $oLanguage->deleteLanguageString($aItem['text']);
+            }
+
+            $this->_oModule->_oDb->deleteHelpTourItems($iEntry);
+            $this->_delete($iEntry);
+        }
+
+        echoJson(array(
+            'grid' => $this->getCode(false),
+        ));
+    }
+
+    private function getAddEditFormObject($iEntry) {
+        $aForm = [
+            'form_attrs' => [
+                'method' => 'post',
+                'action' => '',
+                'id' => 'bx-help-tour-details-form',
+            ],
+            'params' => [
+                'db' => [
+                    'submit_name' => 'submit',
+                    'table' => 'bx_help_tours',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                ],
+            ],
+            'inputs' => [
+                'id' => [
+                    'type' => 'hidden',
+                    'name' => 'id',
+                    'value' => $iEntry,
+                ],
+                'page' => [
+                    'type' => 'select',
+                    'name' => 'page',
+                    'required' => true,
+                    'caption' => _t('_bx_help_tours_form_field_page'),
+                    'values' => ['' => _t('_sys_please_select')] + $this->_oModule->_oDb->getSitePages(),
+                    'checker' => [
+                        'func' => 'Avail',
+                        'error' => _t('_bx_help_tours_form_field_page_error'),
+                    ],
+                    'db' => [
+                        'pass' => 'Xss',
+                    ],
+                ],
+                'overlay' => [
+                    'type' => 'switcher',
+                    'name' => 'overlay',
+                    'caption' => _t('_bx_help_tours_form_field_overlay'),
+                    'info' => _t('_bx_help_tours_form_field_overlay_info'),
+                    'value' => 1,
+                    'db' => [
+                        'pass' => 'Int',
+                    ],
+                ],
+                'actions' => [
+                    'type' => 'input_set',
+                    0 => [
+                        'type' => 'submit',
+                        'name' => 'submit',
+                        'value' => _t('_Submit'),
+                    ],
+                    1 => [
+                        'type' => 'button',
+                        'name' => 'cancel',
+                        'value' => _t('_Cancel'),
+                        'attrs' => [
+                            'style' => 'margin-left: 20px;',
+                            'onclick' => '$(".bx-popup-active:visible").dolPopupHide();',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        return new BxTemplFormView($aForm);
+    }
+}
+
+/** @} */

+ 46 - 0
modules/boonex/help_tours/classes/BxHelpToursModule.php

@@ -0,0 +1,46 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursModule extends BxDolModule
+{
+    public function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    public function serviceGetHelpTour($iTourId) {
+        if (!isLogged() || !$iTourId) return;
+
+        $aTour = $this->_oDb->getTourDetails($iTourId);
+        if (!$aTour) return;
+
+        $aHelpTourItems = $this->_oDb->getHelpTourItems($iTourId);
+        if (!$aHelpTourItems) return;
+
+        if ((!isAdmin() || !bx_get('help_tour_preview')) && $this->_oDb->isHelpTourSeen(getLoggedId(), $iTourId)) return;
+
+        return $this->_oTemplate->getHelpTourCode($aTour, $aHelpTourItems);
+    }
+
+    public function serviceResponseAccountDelete($oAlert) {
+        if ('account' != $oAlert->sUnit || 'delete' != $oAlert->sAction) return;
+
+        $this->_oDb->deleteAccountData($oAlert->iObject);
+    }
+
+    public function actionTourSeen() {
+        if (!isLogged() || !$iTourId = bx_get('tour')) return;
+
+        $this->_oDb->trackTourSeen(getLoggedId(), $iTourId);
+    }
+}
+
+/** @} */

+ 47 - 0
modules/boonex/help_tours/classes/BxHelpToursStudioPage.php

@@ -0,0 +1,47 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursStudioPage extends BxTemplStudioModule
+{
+	protected $_sModule;
+	protected $_oModule;
+
+    function __construct($sModule, $mixedPageName, $sPage = "")
+    {
+    	$this->_sModule = 'bx_help_tours';
+    	$this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($sModule, $mixedPageName, $sPage);
+
+		$this->aMenuItems = array(
+            'tours' => array('name' => 'tours', 'icon' => 'info', 'title' => '_bx_help_tours_page_tours'),
+            'items' => array('name' => 'items', 'icon' => 'list', 'title' => '_bx_help_tours_page_items'),
+        );
+    }
+
+	protected function getTours()
+    {
+		bx_import('BxTemplGrid');
+        $oGrid = BxDolGrid::getObjectInstance('bx_help_tours_tours', BxDolStudioTemplate::getInstance());
+        if (!$oGrid) return 'The module have to be reinstalled';
+        return $oGrid->getCode();
+    }
+
+    protected function getItems()
+    {
+        bx_import('BxTemplGrid');
+        $oGrid = BxDolGrid::getObjectInstance('bx_help_tours_items', BxDolStudioTemplate::getInstance());
+        if (!$oGrid) return 'The module have to be reinstalled';
+        return $oGrid->getCode();
+    }
+}
+
+/** @} */

+ 62 - 0
modules/boonex/help_tours/classes/BxHelpToursTemplate.php

@@ -0,0 +1,62 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursTemplate extends BxBaseModGeneralTemplate
+{
+    public function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+    public function getHelpTourCode($aTour, $aHelpTourItems) {
+        $this->addJs('shepherd/js/shepherd.min.js');
+        $this->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'shepherd/css/|shepherd.css');
+
+        $iShowOnlyItem = isAdmin() && bx_get('help_tour_item') ? intval(bx_get('help_tour_item')) : 0;
+
+        $aHelpTourItemsTmpl = [];
+        foreach ($aHelpTourItems as $iCounter => $aItem) {
+            if ($iShowOnlyItem && $iShowOnlyItem != $aItem['id']) continue;
+
+            $aStep = [
+                'id' => 'bx-help-tour-step-'.$aItem['id'],
+                'text' => _t($aItem['text']),
+                'attachTo' => ['element' => $aItem['element'], 'on' => $aItem['arrow']],
+            ];
+
+            if ($aItem['title']) $aStep['title'] = _t($aItem['title']);
+            if ($aItem['element']) $aStep['attachTo'] = ['element' => $aItem['element'], 'on' => $aItem['arrow']];
+
+            $aHelpTourItemsTmpl[] = $aStep;
+        }
+
+        if (!$aHelpTourItemsTmpl) return;
+
+        return $this->parseHtmlByName('tour.html', [
+            'tour_overlay' => $aTour['overlay'] ? 'true' : 'false',
+            'steps' => json_encode($aHelpTourItemsTmpl),
+            'js_obj' => $this->getTourJsObject(),
+            'bx_if:not_preview' => [
+                'condition' => !(isAdmin() && (bx_get('help_tour_preview') || bx_get('help_tour_item'))),
+                'content' => [
+                    'finish_callback_url' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'tour_seen/',
+                    'tour_id' => $aTour['id'],
+                ],
+            ],
+        ]);
+    }
+
+    public function getTourJsObject() {
+        return 'bx_help_tour_obj';
+    }
+}
+
+/** @} */

+ 71 - 0
modules/boonex/help_tours/install/config.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_help_tours',
+    'title' => 'Help Tours',
+    'note' => 'Help Tours module',
+    'version' => '12.0.2.DEV',
+    'vendor' => 'BoonEx',
+	'help_url' => 'http://feed.una.io/?section={module_name}',
+
+    'compatible_with' => array(
+        '12.0.x'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/help_tours/',
+    'home_uri' => 'help_tours',
+
+    'db_prefix' => 'bx_help_tours_',
+    'class_prefix' => 'BxHelpTours',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'Help Tours',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'enable' => array(
+        'check_dependencies' => 1,
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+);
+
+/** @} */

+ 20 - 0
modules/boonex/help_tours/install/installer.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxHelpToursInstaller extends BxDolStudioInstaller
+{
+    public function __construct($aConfig)
+    {
+        parent::__construct($aConfig);
+    }
+}
+
+/** @} */

+ 52 - 0
modules/boonex/help_tours/install/langs/en.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="en" flag="gb" title="English">
+	<string name="_bx_help_tours"><![CDATA[Help Tours]]></string>
+
+	<string name="_bx_help_tours_page_tours"><![CDATA[Help Tours]]></string>
+	<string name="_bx_help_tours_page_items"><![CDATA[Help Items]]></string>
+
+	<string name="_bx_help_tours_cpt_tour_details"><![CDATA[Help Tour Details]]></string>
+	<string name="_bx_help_tours_cpt_tour_item_details"><![CDATA[Help Item Details]]></string>
+	<string name="_bx_help_tours_cpt_items_cnt"><![CDATA[{0} help items]]></string>
+
+	<string name="_bx_help_tours_grid_col_page"><![CDATA[Page]]></string>
+	<string name="_bx_help_tours_grid_col_items"><![CDATA[Items]]></string>
+
+	<string name="_bx_help_tours_grid_col_name"><![CDATA[Help Item Name]]></string>
+	<string name="_bx_help_tours_grid_col_element"><![CDATA[Page block or selector]]></string>
+	<string name="_bx_help_tours_grid_col_title"><![CDATA[Help Title]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview"><![CDATA[Preview Tour]]></string>
+	<string name="_bx_help_tours_grid_action_delete"><![CDATA[Delete Tour]]></string>
+	<string name="_bx_help_tours_grid_action_add"><![CDATA[Create Tour]]></string>
+	<string name="_bx_help_tours_grid_action_edit"><![CDATA[Edit Tour]]></string>
+	<string name="_bx_help_tours_grid_action_edit_items"><![CDATA[Manage Help Items]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview_item"><![CDATA[Preview Item]]></string>
+	<string name="_bx_help_tours_grid_action_edit_item"><![CDATA[Edit Item]]></string>
+	<string name="_bx_help_tours_grid_action_delete_item"><![CDATA[Delete Item]]></string>
+	<string name="_bx_help_tours_grid_action_add_item"><![CDATA[Add Help Item]]></string>
+
+	<string name="_bx_help_tours_form_field_page"><![CDATA[Page]]></string>
+	<string name="_bx_help_tours_form_field_page_error"><![CDATA[The Page is required]]></string>
+	<string name="_bx_help_tours_form_field_overlay"><![CDATA[Dark Overlay]]></string>
+	<string name="_bx_help_tours_form_field_overlay_info"><![CDATA[If enabled then during showing a tour a full page dark overlay will be appearing.]]></string>
+
+	<string name="_bx_help_tours_form_field_name"><![CDATA[Name]]></string>
+	<string name="_bx_help_tours_form_field_name_info"><![CDATA[For showing in studio only]]></string>
+	<string name="_bx_help_tours_form_field_name_error"><![CDATA[The Name is required]]></string>
+	<string name="_bx_help_tours_form_field_title"><![CDATA[Help Box Title]]></string>
+	<string name="_bx_help_tours_form_field_text"><![CDATA[Help Box Text]]></string>
+	<string name="_bx_help_tours_form_field_text_error"><![CDATA[The Text is required]]></string>
+	<string name="_bx_help_tours_form_field_arrow"><![CDATA[Element's arrow position]]></string>
+	<string name="_bx_help_tours_form_field_element"><![CDATA[The page block or custom element for a Help Box]]></string>
+	<string name="_bx_help_tours_form_field_element_info"><![CDATA[If empty then the Help Box will appear centered in a page]]></string>
+	<string name="_bx_help_tours_form_field_element_custom"><![CDATA[Custom selector]]></string>
+	<string name="_bx_help_tours_form_field_element_custom_info"><![CDATA[For experienced users.<br />This may be an id, f.ex. "#bx-menu-toolbar-2-container".<br />Or a class based selector, f.ex ".bx-menu-object-sys_toolbar_member"]]></string>
+
+	<string name="_bx_help_tours_arrow_option_none"><![CDATA[None]]></string>
+	<string name="_bx_help_tours_form_field_option_none"><![CDATA[None]]></string>
+	<string name="_bx_help_tours_form_field_option_custom"><![CDATA[Custom]]></string>
+
+	<string name="_bx_help_tours_page_block_caption"><![CDATA[Help Tour]]></string>
+</resources>

+ 4 - 0
modules/boonex/help_tours/install/sql/disable.sql

@@ -0,0 +1,4 @@
+-- GRIDS
+DELETE FROM `sys_objects_grid` WHERE `object` LIKE 'bx_help_tours_%';
+DELETE FROM `sys_grid_fields` WHERE `object` LIKE 'bx_help_tours_%';
+DELETE FROM `sys_grid_actions` WHERE `object` LIKE 'bx_help_tours_%';

+ 38 - 0
modules/boonex/help_tours/install/sql/enable.sql

@@ -0,0 +1,38 @@
+-- GRID: help tours
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`, `show_total_count`) VALUES
+('bx_help_tours_tours', 'Sql', 'SELECT * FROM `bx_help_tours` WHERE 1', 'bx_help_tours', 'id', 'order', '', '', 100, NULL, 'start', '', '', '', 'auto', '', '', 2147483647, 'BxHelpToursGridTours', 'modules/boonex/help_tours/classes/BxHelpToursGridTours.php', 0);
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_help_tours_tours', 'checkbox', '', '2%', 0, 0, '', 1),
+('bx_help_tours_tours', 'order', '', '1%', 0, 0, '', 2),
+('bx_help_tours_tours', 'page', '_bx_help_tours_grid_col_page', '37%', 0, 0, '', 3),
+('bx_help_tours_tours', 'items', '_bx_help_tours_grid_col_items', '30%', 0, 0, '', 4),
+('bx_help_tours_tours', 'actions', '', '30%', 0, 0, '', 5);
+
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_help_tours_tours', 'single', 'preview', '_bx_help_tours_grid_action_preview', 'eye', 1, 0, 1),
+('bx_help_tours_tours', 'single', 'edit', '_bx_help_tours_grid_action_edit', 'edit', 1, 0, 2),
+('bx_help_tours_tours', 'single', 'edit_items', '_bx_help_tours_grid_action_edit_items', 'list', 1, 0, 3),
+('bx_help_tours_tours', 'single', 'delete', '_bx_help_tours_grid_action_delete', 'remove', 1, 1, 4),
+('bx_help_tours_tours', 'bulk', 'delete', '_bx_help_tours_grid_action_delete', 'remove', 0, 1, 1),
+('bx_help_tours_tours', 'independent', 'add', '_bx_help_tours_grid_action_add', 'plus', 0, 0, 1);
+
+-- GRID: help tour items
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`, `show_total_count`) VALUES
+('bx_help_tours_items', 'Sql', 'SELECT * FROM `bx_help_tours_items` WHERE 1', 'bx_help_tours_items', 'id', 'order', '', '', 100, NULL, 'start', '', 'element', 'title,text', 'auto', '', '', 2147483647, 'BxHelpToursGridItems', 'modules/boonex/help_tours/classes/BxHelpToursGridItems.php', 0);
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_help_tours_items', 'checkbox', '', '2%', 0, 0, '', 1),
+('bx_help_tours_items', 'order', '', '1%', 0, 0, '', 2),
+('bx_help_tours_items', 'name', '_bx_help_tours_grid_col_name', '32%', 0, 0, '', 3),
+('bx_help_tours_items', 'element', '_bx_help_tours_grid_col_element', '18%', 0, 0, '', 4),
+('bx_help_tours_items', 'title', '_bx_help_tours_grid_col_title', '32%', 1, 0, '', 5),
+('bx_help_tours_items', 'actions', '', '15%', 0, 0, '', 6);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_help_tours_items', 'single', 'preview', '_bx_help_tours_grid_action_preview_item', 'eye', 1, 0, 1),
+('bx_help_tours_items', 'single', 'edit', '_bx_help_tours_grid_action_edit_item', 'edit', 1, 0, 2),
+('bx_help_tours_items', 'single', 'delete', '_bx_help_tours_grid_action_delete_item', 'remove', 1, 1, 3),
+('bx_help_tours_items', 'bulk', 'delete', '_bx_help_tours_grid_action_delete_item', 'remove', 0, 1, 1),
+('bx_help_tours_items', 'independent', 'add', '_bx_help_tours_grid_action_add_item', 'plus', 0, 0, 1);

+ 48 - 0
modules/boonex/help_tours/install/sql/install.sql

@@ -0,0 +1,48 @@
+-- Studio page and widget
+INSERT INTO `sys_std_pages`(`index`, `name`, `header`, `caption`, `icon`) VALUES
+(3, 'bx_help_tours', '_bx_help_tours', '_bx_help_tours', 'bx_help_tours@modules/boonex/help_tours/|std-icon.svg');
+SET @iPageId = LAST_INSERT_ID();
+
+SET @iParentPageId = (SELECT `id` FROM `sys_std_pages` WHERE `name` = 'home');
+SET @iParentPageOrder = (SELECT MAX(`order`) FROM `sys_std_pages_widgets` WHERE `page_id` = @iParentPageId);
+INSERT INTO `sys_std_widgets` (`page_id`, `module`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
+(@iPageId, 'bx_help_tours', '{url_studio}module.php?name=bx_help_tours&page=tours', '', 'bx_help_tours@modules/boonex/help_tours/|std-icon.svg', '_bx_help_tours', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+INSERT INTO `sys_std_pages_widgets` (`page_id`, `widget_id`, `order`) VALUES
+(@iParentPageId, LAST_INSERT_ID(), IF(ISNULL(@iParentPageOrder), 1, @iParentPageOrder + 1));
+
+-- Main Tables
+CREATE TABLE `bx_help_tours` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `overlay` tinyint(1) NOT NULL,
+  `page` varchar(128) NOT NULL,
+  `order` int(11) NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `bx_help_tours_items` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `tour` int(11) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `element` varchar(255) NOT NULL,
+  `arrow` ENUM('auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end'),
+  `title` varchar(128) NOT NULL,
+  `text` varchar(128) NOT NULL,
+  `order` int(11) NOT NULL,
+  KEY `tour` (`tour`),
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `bx_help_tours_track_views` (
+  `account` int(11) NOT NULL,
+  `tour` int(11) NOT NULL,
+  KEY `account` (`account`),
+  PRIMARY KEY (`account`, `tour`)
+);
+
+-- ACCOUNT REMOVAL ALERT
+INSERT INTO `sys_alerts_handlers` (`name`, `class`, `file`, `service_call`) VALUES
+('bx_help_tours_account_delete', '', '', 'a:3:{s:6:"module";s:13:"bx_help_tours";s:6:"method";s:23:"response_account_delete";s:6:"params";s:5:"$this";}');
+SET @iHandler := LAST_INSERT_ID();
+
+INSERT INTO `sys_alerts` (`unit`, `action`, `handler_id`) VALUES
+('account', 'delete', @iHandler);

+ 17 - 0
modules/boonex/help_tours/install/sql/uninstall.sql

@@ -0,0 +1,17 @@
+-- Studio page and widget
+DELETE FROM `tp`, `tw`, `tpw`
+USING `sys_std_pages` AS `tp`, `sys_std_widgets` AS `tw`, `sys_std_pages_widgets` AS `tpw`
+WHERE `tp`.`id` = `tw`.`page_id` AND `tw`.`id` = `tpw`.`widget_id` AND `tp`.`name` = 'bx_help_tours';
+
+-- Main Tables
+DROP TABLE IF EXISTS `bx_help_tours`;
+DROP TABLE IF EXISTS `bx_help_tours_items`;
+DROP TABLE IF EXISTS `bx_help_tours_track_views`;
+
+-- Help Tours page blocks
+DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_help_tours';
+
+-- ACCOUNT REMOVAL ALERT
+SET @iHandler := (SELECT `id` FROM `sys_alerts_handlers` WHERE `name` = 'bx_help_tours_account_delete' LIMIT 1);
+DELETE FROM `sys_alerts` WHERE `handler_id` = @iHandler;
+DELETE FROM `sys_alerts_handlers` WHERE `id` = @iHandler;

+ 47 - 0
modules/boonex/help_tours/js/grid_items.js

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+function BxHelpToursGridItems(oOptions) {
+    this.sModuleUrl = oOptions.sModuleUrl;
+    this.sActionsUrl = oOptions.sActionUrl;
+    this.sPageUrl = oOptions.sPageUrl;
+    this.sObjNameGrid = oOptions.sObjNameGrid;
+    this.sObjName = oOptions.sObjName == undefined ? 'oAqbConditionalFieldsGridValues' : oOptions.sObjName;
+    this.sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this.iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this.iTour = oOptions.iTour == undefined ? '' : oOptions.iTour;
+}
+
+BxHelpToursGridItems.prototype.onChangeTour = function (v) {
+    this.iTour = v;
+    this.reloadGrid();
+};
+
+BxHelpToursGridItems.prototype.reloadGrid = function () {
+    var bReload = false;
+
+    var oActions = $("[bx_grid_action_independent]");
+    if(!this.iTour){
+        oActions.addClass('bx-btn-disabled');
+    } else {
+        oActions.removeClass('bx-btn-disabled');
+    }
+
+    if (glGrids[this.sObjNameGrid]._oQueryAppend['tour'] != this.iTour) {
+        glGrids[this.sObjNameGrid]._oQueryAppend['tour'] = this.iTour;
+        bReload = true;
+    }
+
+    if (bReload) {
+        glGrids[this.sObjNameGrid].reload(0);
+    }
+};
+/** @} */

+ 16 - 0
modules/boonex/help_tours/request.php

@@ -0,0 +1,16 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Help Tours Help Tours
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+check_logged();
+
+BxDolRequest::processAsAction($GLOBALS['aModule'], $GLOBALS['aRequest']);
+
+/** @} */

+ 6 - 0
modules/boonex/help_tours/template/css/admin.css

@@ -0,0 +1,6 @@
+div.bx-grid-header div.bx-form-element-wrapper {
+    position: relative;
+    float: left;
+    margin-top: 0px;
+    margin-right: 20px;
+}

+ 10 - 0
modules/boonex/help_tours/template/grid_items.html

@@ -0,0 +1,10 @@
+__content__
+<script language="javascript">
+    var __js_object__ = new BxHelpToursGridItems({
+        sModuleUrl: '__module_url__',
+        sActionUrl: '<bx_url_root />grid.php',
+        sPageUrl: '__page_url__',
+        sObjNameGrid: '__grid_object__',
+        iTour: '__tour__'
+    });
+</script>

+ 27 - 0
modules/boonex/help_tours/template/images/icons/std-icon.svg

@@ -0,0 +1,27 @@
+<svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 75C0 18.75 18.75 0 75 0C131.25 0 150 18.75 150 75C150 131.25 131.25 150 75 150C18.75 150 0 131.25 0 75Z" fill="url(#paint0_linear)"/>
+<g filter="url(#filter0_d)">
+<path d="M101.46 79.5407C108.165 86.246 107.442 97.5539 99.3006 103.272C94.1194 106.909 86.8823 106.909 81.7011 103.272C73.5549 97.5591 72.8368 86.246 79.5419 79.5407C85.5961 73.4864 95.4108 73.4864 101.46 79.5407" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M112 112L102 102" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M50.26 48.96C50.248 48.96 50.24 48.968 50.24 48.98C50.24 48.992 50.248 49 50.26 49C50.272 49 50.28 48.992 50.28 48.98C50.28 48.968 50.272 48.96 50.26 48.96" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M60.432 48.96C60.42 48.96 60.412 48.968 60.412 48.98C60.412 48.992 60.424 49 60.432 49C60.444 49 60.452 48.992 60.452 48.98C60.452 48.968 60.444 48.96 60.432 48.96" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M70.588 48.96C70.576 48.96 70.568 48.968 70.568 48.98C70.568 48.992 70.576 49 70.588 49C70.6 49 70.608 48.992 70.608 48.98C70.608 48.968 70.6 48.96 70.588 48.96" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M111 71V47C111 42.58 107.42 39 103 39H47C42.58 39 39 42.58 39 47V103C39 107.42 42.58 111 47 111H71" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M39.144 59H111" stroke="#F3F4F6" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<filter id="filter0_d" x="34" y="35" width="83" height="83" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1"/>
+<feGaussianBlur stdDeviation="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
+</filter>
+<linearGradient id="paint0_linear" x1="75" y1="0" x2="75" y2="150" gradientUnits="userSpaceOnUse">
+<stop stop-color="#10B981"/>
+<stop offset="1" stop-color="#059669"/>
+</linearGradient>
+</defs>
+</svg>

+ 70 - 0
modules/boonex/help_tours/template/tour.html

@@ -0,0 +1,70 @@
+<script defer language="javascript">
+    __js_obj__ = new Shepherd.Tour({
+        useModalOverlay: __tour_overlay__,
+        defaultStepOptions: {
+            classes: 'bx-def-z-index-modal',
+            showCancelLink: true,
+            scrollTo: {behavior: "smooth", block: "center"}
+        }
+    })
+
+    <bx_if:not_preview>
+    Shepherd.on('cancel', function () {
+        $.post('__finish_callback_url__', {tour: __tour_id__});
+    })
+    </bx_if:not_preview>
+
+    var bx_help_tour_steps =  __steps__;
+    var bx_help_tour_steps_filtered = [];
+    for (i in bx_help_tour_steps) {
+        //do not show help items for missing elements.
+        if (bx_help_tour_steps[i].attachTo.element.length && !$(bx_help_tour_steps[i].attachTo.element).length) continue;
+
+        // if this is a help for a field that is sitting inside a collapsed section then
+        // we are `uncollapsing` it on this page load
+        if (!$(bx_help_tour_steps[i].attachTo.element).is(':visible')) {
+            var sect = $(bx_help_tour_steps[i].attachTo.element).closest('.bx-form-section-hidden');
+            if (sect.length && sect.hasClass('bx-form-section-hidden')) {
+                $(sect).children('legend').trigger('click');
+                sect.removeClass('bx-form-collapsed');
+                sect.removeClass('bx-form-section-hidden');
+            }
+        }
+
+        //do not process hidden elements
+        if (!$(bx_help_tour_steps[i].attachTo.element).is(':visible')) continue;
+
+        bx_help_tour_steps_filtered.push(bx_help_tour_steps[i]);
+    }
+    bx_help_tour_steps = bx_help_tour_steps_filtered;
+
+    if (bx_help_tour_steps_filtered) {
+        var bx_help_tour_steps_active = 0;
+        for (i in bx_help_tour_steps) {
+            var aBtns = [];
+            if (i != (bx_help_tour_steps.length - 1)) {
+                aBtns.push({
+                    text: '<bx_text_js:_sys_exit />',
+                    action: __js_obj__.cancel,
+                    classes: 'shepherd-button-secondary'
+                });
+                aBtns.push({
+                    text: '<bx_text_js:_sys_next />',
+                    action: __js_obj__.next,
+                    classes: 'shepherd-button-primary'
+                });
+            } else {
+                aBtns.push({
+                    text: '<bx_text_js:_sys_done />',
+                    action: __js_obj__.cancel,
+                    classes: 'shepherd-button-primary'
+                });
+            }
+
+            bx_help_tour_steps[i].buttons = aBtns;
+            __js_obj__.addStep(bx_help_tour_steps[i]);
+            bx_help_tour_steps_active++;
+        }
+        if (bx_help_tour_steps_active) __js_obj__.start();
+    }
+</script>

+ 52 - 0
modules/boonex/russian/data/langs/bx_help_tours/ru.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="ru" flag="ru" title="Russian">
+    <string name="_bx_help_tours"><![CDATA[Туры помощи]]></string>
+
+	<string name="_bx_help_tours_page_tours"><![CDATA[Туры помощи]]></string>
+	<string name="_bx_help_tours_page_items"><![CDATA[Элементы помощи]]></string>
+
+	<string name="_bx_help_tours_cpt_tour_details"><![CDATA[Детали тура помощи]]></string>
+	<string name="_bx_help_tours_cpt_tour_item_details"><![CDATA[Детали эелемента помощи]]></string>
+	<string name="_bx_help_tours_cpt_items_cnt"><![CDATA[элементов помощи: {0}]]></string>
+
+	<string name="_bx_help_tours_grid_col_page"><![CDATA[Страница]]></string>
+	<string name="_bx_help_tours_grid_col_items"><![CDATA[Элементы]]></string>
+
+	<string name="_bx_help_tours_grid_col_name"><![CDATA[Имя элемента помощи]]></string>
+	<string name="_bx_help_tours_grid_col_element"><![CDATA[Блок на странице или селектор]]></string>
+	<string name="_bx_help_tours_grid_col_title"><![CDATA[Заголовок]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview"><![CDATA[Предпросмотр тура]]></string>
+	<string name="_bx_help_tours_grid_action_delete"><![CDATA[Удалить тур]]></string>
+	<string name="_bx_help_tours_grid_action_add"><![CDATA[Создать тур]]></string>
+	<string name="_bx_help_tours_grid_action_edit"><![CDATA[Изменить тур]]></string>
+	<string name="_bx_help_tours_grid_action_edit_items"><![CDATA[Редактировать элемент помощи]]></string>
+
+	<string name="_bx_help_tours_grid_action_preview_item"><![CDATA[Предпросмотр элемента]]></string>
+	<string name="_bx_help_tours_grid_action_edit_item"><![CDATA[Редактировать элемент]]></string>
+	<string name="_bx_help_tours_grid_action_delete_item"><![CDATA[Удалить элемент]]></string>
+	<string name="_bx_help_tours_grid_action_add_item"><![CDATA[Добавить элемент помощи]]></string>
+
+	<string name="_bx_help_tours_form_field_page"><![CDATA[Страница]]></string>
+	<string name="_bx_help_tours_form_field_page_error"><![CDATA[Страница является обязательным полем.]]></string>
+	<string name="_bx_help_tours_form_field_overlay"><![CDATA[Темный фон]]></string>
+	<string name="_bx_help_tours_form_field_overlay_info"><![CDATA[Если включено, то во время показа тура страница будет затемнена.]]></string>
+
+	<string name="_bx_help_tours_form_field_name"><![CDATA[Имя]]></string>
+	<string name="_bx_help_tours_form_field_name_info"><![CDATA[Только для показа в студии.]]></string>
+	<string name="_bx_help_tours_form_field_name_error"><![CDATA[Имя является необходимым полем.]]></string>
+	<string name="_bx_help_tours_form_field_title"><![CDATA[Заголовок элемента помощи]]></string>
+	<string name="_bx_help_tours_form_field_text"><![CDATA[Текст элемента помощи]]></string>
+	<string name="_bx_help_tours_form_field_text_error"><![CDATA[Текст является необходимым полем]]></string>
+	<string name="_bx_help_tours_form_field_arrow"><![CDATA[Позиция стрелки элемента помощи]]></string>
+	<string name="_bx_help_tours_form_field_element"><![CDATA[Блок на странице или пользовательский элемент для показа блока помощи]]></string>
+	<string name="_bx_help_tours_form_field_element_info"><![CDATA[Если не задано, то блок помощи будет показан посередине страницы.]]></string>
+	<string name="_bx_help_tours_form_field_element_custom"><![CDATA[Пользовательский селектор]]></string>
+	<string name="_bx_help_tours_form_field_element_custom_info"><![CDATA[Для продвинутых пользователей.<br />Это мжет быть id, например "#bx-menu-toolbar-2-container".<br />Или селектор, основанный на CSS классах, например ".bx-menu-object-sys_toolbar_member"]]></string>
+
+	<string name="_bx_help_tours_arrow_option_none"><![CDATA[Нет]]></string>
+	<string name="_bx_help_tours_form_field_option_none"><![CDATA[Нет]]></string>
+	<string name="_bx_help_tours_form_field_option_custom"><![CDATA[Пользовательский]]></string>
+
+	<string name="_bx_help_tours_page_block_caption"><![CDATA[Тур Помощи]]></string>
+</resources>