diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d53fe88 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2015 DigitalWand (http://digitalwand.ru/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/admin/route.php b/admin/route.php index 1fe6277..2772842 100644 --- a/admin/route.php +++ b/admin/route.php @@ -1,79 +1,136 @@ -GetCurPage()); -if (isset($_SESSION["SESS_SORT_BY"][$uniq])) { - unset($_SESSION["SESS_SORT_BY"][$uniq]); -} -if (isset($_SESSION["SESS_SORT_ORDER"][$uniq])) { - unset($_SESSION["SESS_SORT_ORDER"][$uniq]); -} - -$module = getRequestParams('module'); -$view = getRequestParams('view'); -if (!$module OR !$view OR !Loader::IncludeModule($module)) { - include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php'; -} - -list($helper, $interface) = AdminBaseHelper::getGlobalInterfaceSettings($module, $view); - -if (!$helper OR !$interface) { - include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php'; -} - -$isPopup = isset($_REQUEST['popup']) AND $_REQUEST['popup'] == 'Y'; -$fields = isset($interface['FIELDS']) ? $interface['FIELDS'] :array(); -$tabs = isset($interface['TABS']) ? $interface['TABS'] :array(); -$helperType = false; - - -if (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminEditHelper')) { - $helperType = 'edit'; - /** @var AdminEditHelper $adminHelper */ - $adminHelper = new $helper($fields, $tabs); - -} else if (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminListHelper')) { - $helperType = 'list'; - /** @var AdminListHelper $adminHelper */ - $adminHelper = new $helper($fields, $isPopup); - $adminHelper->getData(array($by => $order)); - -} else { - include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php'; - exit(); -} - -if ($isPopup) { - require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_popup_admin.php"); -} else { - require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_admin_after.php"); -} - -if ($helperType == 'list') { - $adminHelper->createFilterForm(); -} -$adminHelper->show(); - -if ($isPopup) { - require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_popup_admin.php"); -} else { - require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_admin.php"); -} - +GetCurPage()); + +if (isset($_SESSION["SESS_SORT_BY"][$uniq])) { + unset($_SESSION["SESS_SORT_BY"][$uniq]); +} +if (isset($_SESSION["SESS_SORT_ORDER"][$uniq])) { + unset($_SESSION["SESS_SORT_ORDER"][$uniq]); +} + +$module = getRequestParams('module'); +$view = getRequestParams('view'); + +if (!$module OR !$view OR !Loader::IncludeModule($module)) { + include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php'; +} + +// Собираем имя класса админского интерфейса +$moduleNameParts = explode('.', $module); +$entityNameParts = explode('_', $entity); +$interfaceNameParts = array_merge($moduleNameParts, $entityNameParts); +$interfaceNameClass = null; +$viewParts = explode('_', $view); + +$count = count($viewParts); +for ($i = 0; $i < $count; $i++) { + $interfaceName = implode('', array_map('ucfirst', $viewParts)); + $parts = $interfaceNameParts; + $parts[] = $interfaceName . 'AdminInterface'; + $class = array_map('ucfirst', $parts); + $interfaceNameClass = implode('\\', $class); + + if (class_exists($interfaceNameClass)) { + break; + } + else { + $className = array_pop($parts); + $parts[] = 'AdminInterface'; + $parts[] = $className; + $class = array_map('ucfirst', $parts); + $interfaceNameClass = implode('\\', $class); + if (class_exists($interfaceNameClass)) { + break; + } + } + array_pop($viewParts); +} + +/** + * @var AdminInterface $interfaceNameClass + */ + +if ($interfaceNameClass && class_exists($interfaceNameClass)) { + $interfaceNameClass::register(); +} + +list($helper, $interface) = AdminBaseHelper::getGlobalInterfaceSettings($module, $view); + +if (!$helper OR !$interface) { + include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php'; +} + +$isPopup = isset($_REQUEST['popup']) AND $_REQUEST['popup'] == 'Y'; +$fields = isset($interface['FIELDS']) ? $interface['FIELDS'] : array(); +$tabs = isset($interface['TABS']) ? $interface['TABS'] : array(); +$helperType = false; + +if (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminEditHelper')) { + $helperType = 'edit'; + /** + * @var AdminEditHelper $adminHelper + */ + $adminHelper = new $helper($fields, $tabs); +} +elseif (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminListHelper')) { + $helperType = 'list'; + /** + * @var AdminListHelper $adminHelper + */ + $adminHelper = new $helper($fields, $isPopup); + $adminHelper->buildList(array($by => $order)); +} +elseif (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminBaseHelper')) { + $adminHelper = new $helper($fields, $tabs); +} +else { + include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php'; + exit(); +} + +if ($isPopup) { + require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_popup_admin.php"); +} +else { + require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_admin_after.php"); +} + +if ($helperType == 'list') { + $adminHelper->createFilterForm(); +} + +$adminHelper->show(); + +if ($isPopup) { + require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_popup_admin.php"); +} +else { + require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_admin.php"); +} \ No newline at end of file diff --git a/include.php b/include.php index 9750bc2..c731cf4 100644 --- a/include.php +++ b/include.php @@ -6,21 +6,30 @@ array( 'DigitalWand\AdminHelper\EventHandlers' => 'lib/EventHandlers.php', + 'DigitalWand\AdminHelper\Helper\Exception' => 'lib/helper/Exception.php', + + 'DigitalWand\AdminHelper\Helper\AdminInterface' => 'lib/helper/AdminInterface.php', 'DigitalWand\AdminHelper\Helper\AdminBaseHelper' => 'lib/helper/AdminBaseHelper.php', 'DigitalWand\AdminHelper\Helper\AdminListHelper' => 'lib/helper/AdminListHelper.php', + 'DigitalWand\AdminHelper\Helper\AdminSectionListHelper' => 'lib/helper/AdminSectionListHelper.php', 'DigitalWand\AdminHelper\Helper\AdminEditHelper' => 'lib/helper/AdminEditHelper.php', - 'DigitalWand\AdminHelper\Helper\Exception' => 'lib/helper/Exception.php', + 'DigitalWand\AdminHelper\Helper\AdminSectionEditHelper' => 'lib/helper/AdminSectionEditHelper.php', + + 'DigitalWand\AdminHelper\EntityManager' => 'lib/EntityManager.php', 'DigitalWand\AdminHelper\Widget\HelperWidget' => 'lib/widget/HelperWidget.php', 'DigitalWand\AdminHelper\Widget\CheckboxWidget' => 'lib/widget/CheckboxWidget.php', 'DigitalWand\AdminHelper\Widget\ComboBoxWidget' => 'lib/widget/ComboBoxWidget.php', 'DigitalWand\AdminHelper\Widget\StringWidget' => 'lib/widget/StringWidget.php', 'DigitalWand\AdminHelper\Widget\NumberWidget' => 'lib/widget/NumberWidget.php', - 'DigitalWand\AdminHelper\Widget\ImageWidget' => 'lib/widget/ImageWidget.php', 'DigitalWand\AdminHelper\Widget\FileWidget' => 'lib/widget/FileWidget.php', 'DigitalWand\AdminHelper\Widget\TextAreaWidget' => 'lib/widget/TextAreaWidget.php', - 'DigitalWand\AdminHelper\Widget\VisualEditorWidget' => 'lib/widget/VisualEditorWidget.php', - 'DigitalWand\AdminHelper\Widget\DateTimeWidget' => 'lib/widget/DateTimeWidget.php', 'DigitalWand\AdminHelper\Widget\HLIBlockFieldWidget' => 'lib/widget/HLIBlockFieldWidget.php', + 'DigitalWand\AdminHelper\Widget\DateTimeWidget' => 'lib/widget/DateTimeWidget.php', + 'DigitalWand\AdminHelper\Widget\IblockElementWidget' => 'lib/widget/IblockElementWidget.php', + 'DigitalWand\AdminHelper\Widget\UrlWidget' => 'lib/widget/UrlWidget.php', + 'DigitalWand\AdminHelper\Widget\VisualEditorWidget' => 'lib/widget/VisualEditorWidget.php', + 'DigitalWand\AdminHelper\Widget\UserWidget' => 'lib/widget/UserWidget.php', + 'DigitalWand\AdminHelper\Widget\OrmElementWidget' => 'lib/widget/OrmElementWidget.php', ) ); diff --git a/install/admin/admin_helper_route.php b/install/admin/admin_helper_route.php index fc51631..375e55b 100644 --- a/install/admin/admin_helper_route.php +++ b/install/admin/admin_helper_route.php @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/lang/ru/lib/helper/AdminBaseHelper.php b/lang/ru/lib/helper/AdminBaseHelper.php index 3ac680b..457103d 100644 --- a/lang/ru/lib/helper/AdminBaseHelper.php +++ b/lang/ru/lib/helper/AdminBaseHelper.php @@ -1,3 +1,4 @@ 'Монстры на каникулах 2', + * 'YEAR' => 2015, + * // У сущности FilmTable есть связь с RelatedLinksTable через поле RELATED_LINKS. + * // Если передать ей данные, то они будут обработаны + * // Представим, что у сущности RelatedLinksTable есть поля ID и VALUE (в этом поле хранится ссылка), FILM_ID + * // В большинстве случаев, данные передаваемые связям генерируются множественными виджетами + * 'RELATED_LINKS' => array( + * // Переданный ниже массив будет обработан аналогично коду RelatedLinksTable::add(array('VALUE' => + * 'yandex.ru')); array('VALUE' => 'yandex.ru'), + * // Если в массив добавить ID, то запись обновится: RelatedLinksTable::update(3, array('ID' => 3, 'VALUE' + * => 'google.com')); array('ID' => 3, 'VALUE' => 'google.com'), + * // ВНИМАНИЕ: данный класс реководствуется принципом: что передано для связи, то сохранится или обновится, + * что не передано, будет удалено + * // То есть, если в поле связи RELATED_LINKS передать пустой массив, то все значения связи будут удалены + * ) + * )); + * $filmManager->save(); + * ``` + * + * Пример удаления сущности + * ``` + * $articleManager = new EntityManager('\Vendor\Module\ArticlesTable', array(), 7, $adminHelper); + * $articleManager->delete(); + * ``` + * + * Как работает сохранение данных ? Дополнительный пример + * Допустим, что есть модели NewsTable (новости) и NewsLinksTable (ссылки на дополнительную информацию о новости) + * + * У модели NewsTable есть связь с моделью NewsLinksTable через поле NEWS_LINKS: + * ``` + * DataManager::getMap() { + * ... + * new Entity\ReferenceField( + * 'NEWS_LINKS', + * '\Vendor\Module\NewsLinksTable', + * array('=this.ID' => 'ref.NEWS_ID'), + * 'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'), + * 'ref.ENTITY' => new DB\SqlExpression('?s', 'news'), + * ), + * ... + * } + * ``` + * + * Попробуем сохранить + * ``` + * $newsManager = new EntityManager( + * '\Vendor\Module\NewsTable', + * array( + * 'TITLE' => 'News title', + * 'CONTENT' => 'News content', + * 'NEWS_LINKS' => array( + * array('LINK' => 'test.ru'), + * array('LINK' => 'test2.ru'), + * array('ID' => 'id ссылки', 'LINK' => 'test3.ru'), + * ) + * ), + * null, + * $adminHelper + * ); + * $newsManager->save(); + * ``` + * + * В данном примере передаются данные для новости (заголовок и содержимое) и данные для поля-связи NEWS_LINKS. + * + * Далее EntityManager: + * 1. Вырезает данные, которые предназначены связям + * 2. Подставляет в них данные из основной модели на основе условий связи + * Например для связи с полем NEWS_LINKS подставятся данные: + * + * ``` + * NewsLinksTable::ENTITY_ID => NewsTable::ID, + * NewsLinksTable::FIELD => 'NEWS_LINKS', + * NewsLinksTable::ENTITY => 'news' + * ``` + * + * 3. После подстановки данных они будут переданы модели связи подобно коду ниже: + * + * ``` + * NewsLinksTable::add(array('ENTITY' => 'news', 'FIELD' => 'NEWS_LINKS', 'ENTITY_ID' => 'id сущности, например + * новости', 'LINK' => 'test.ru')); NewsLinksTable::add(array('ENTITY' => 'news', 'FIELD' => 'NEWS_LINKS', 'ENTITY_ID' + * => 'id сущности', 'LINK' => 'test2.ru')); NewsLinksTable::update('id ссылки', array('ENTITY' => 'news', 'FIELD' => + * 'NEWS_LINKS', 'ENTITY_ID' => 'id сущности', 'LINK' => 'test3.ru')); + * ``` + * + * Обратите внимание, что в метод EntityManager::save() были изначально передано только поле LINK, поля ENTITY, + * ENTITY_ID и FIELD были подставлены классом EntityManager автоматически (предыдущий пункт) А так же важно, что для + * третьей ссылки был передан идентификатор, поэтому выполнился NewsLinksTable::update, а не NewsLinksTable::add + * + * 4. Далее `EntityManager` удаляет данные связанной модели `NewsLinksTable`, которые не были добавлены или обновлены. + * + * Как работает удаление? + * + * 1. EntityManager получает из `NewsTable::getMap()` поля-связи + * 2. Получает поля описанные в интерфейсе генератора админки + * 3. Удаляет значения для полей-связей, которые объявлены в интерфейсе + * + * Примечание. + * EntityManager управляет только данными, которые получает при помощи связи стандартными средставами битрикса. + * Например, при удалении NewsTable будут удалены только NewsLinksTable, где: + * + * ``` + * NewsLinksTable::ENTITY_ID => NewsTable::ID, + * NewsLinksTable::FIELD => 'NEWS_LINKS', + * NewsLinksTable::ENTITY => 'news' + * ``` + * + * @author Nik Samokhvalov + * @author Dmitriy Baibuhtin + */ +class EntityManager +{ + /** + * @var string Класс модели. + */ + protected $modelClass; + /** + * @var Entity\Base Сущность модели. + */ + protected $model; + /** + * @var array Данные для обработки. + */ + protected $data; + /** + * @var integer Идентификатор записи. + */ + protected $itemId = null; + /** + * @var string Поле модели, в котором хранится идентификатор записи. + */ + protected $modelPk = null; + /** + * @var array Данные для связей. + */ + protected $referencesData; + /** + * @var AdminBaseHelper Хелпер. + */ + protected $helper; + /** + * @var array Предупреждения. + */ + protected $notes = array(); + + /** + * @param string $modelClass Класс основной модели, наследника DataManager. + * @param array $data Массив с сохраняемыми данными. + * @param integer $itemId Идентификатор сохраняемой записи. + * @param AdminBaseHelper $helper Хелпер, инициирующий сохранение записи. + */ + public function __construct($modelClass, array $data = array(), $itemId = null, AdminBaseHelper $helper) + { + Loc::loadMessages(__FILE__); + + $this->modelClass = $modelClass; + $this->model = $modelClass::getEntity(); + $this->data = $data; + $this->modelPk = $this->model->getPrimary(); + $this->helper = $helper; + + if (!empty($itemId)) { + $this->setItemId($itemId); + } + } + + /** + * Сохранить запись и данные связей. + * + * @return Entity\AddResult|Entity\UpdateResult + */ + public function save() + { + $this->collectReferencesData(); + + /** + * @var DataManager $modelClass + */ + $modelClass = $this->modelClass; + $db = $this->model->getConnection(); + $db->startTransaction(); // начало транзакции + + if (empty($this->itemId)) { + $result = $modelClass::add($this->data); + + if ($result->isSuccess()) { + $this->setItemId($result->getId()); + } + } + else { + $result = $modelClass::update($this->itemId, $this->data); + } + + if ($result->isSuccess()) { + $referencesDataResult = $this->processReferencesData(); + if($referencesDataResult->isSuccess()){ + $db->commitTransaction(); // ошибок нет - применяем изменения + }else{ + $result = $referencesDataResult; // возвращаем ReferencesResult что бы вернуть ошибку + $db->rollbackTransaction(); // что-то пошло не так - возвращаем все как было + } + } else { + $db->rollbackTransaction(); + } + + return $result; + } + + /** + * Удаление запись и данные связей. + * + * @return Entity\DeleteResult + */ + public function delete() + { + // Удаление данных зависимостей + $db = $this->model->getConnection(); + $db->startTransaction(); // начало транзакции + + $result = $this->deleteReferencesData(); // удаляем зависимые сущности + + if(!$result->isSuccess()){ // если хотя бы одна из них не удалилась + $db->rollbackTransaction(); // то восстанавливаем все + return $result; // возвращаем ошибку + } + + $model = $this->modelClass; + + $result = $model::delete($this->itemId); // удаляем основную сущность + if(!$result->isSuccess()){ // если не удалилась + $db->rollbackTransaction(); // то восстанавливаем зависимые сущности + return $result; // возвращаем ошибку + } + + $db->commitTransaction(); // все прошло без ошибок применяем изменения + return $result; + } + + /** + * Получить список предупреждений + * @return array + */ + public function getNotes() + { + return $this->notes; + } + + /** + * Добавить предупреждение + * + * @param $note + * @param string $key Ключ для избежания дублирования сообщений + * + * @return bool + */ + protected function addNote($note, $key = null) + { + if ($key) { + $this->notes[$key] = $note; + } + else { + $this->notes[] = $note; + } + + return true; + } + + /** + * Установка текущего идентификатора модели. + * + * @param integer $itemId Идентификатор записи. + */ + protected function setItemId($itemId) + { + $this->itemId = $itemId; + $this->data[$this->modelPk] = $this->itemId; + } + + /** + * Получение связей + * + * @return array + */ + protected function getReferences() + { + /** + * @var DataManager $modelClass + */ + $modelClass = $this->modelClass; + $entity = $modelClass::getEntity(); + $fields = $entity->getFields(); + $references = array(); + + foreach ($fields as $fieldName => $field) { + if ($field instanceof Entity\ReferenceField) { + $references[$fieldName] = $field; + } + } + + return $references; + } + + /** + * Извлечение данных для связей + */ + protected function collectReferencesData() + { + $result = new Entity\Result(); + $references = $this->getReferences(); + // Извлечение данных управляемых связей + foreach ($references as $fieldName => $reference) { + if (array_key_exists($fieldName, $this->data)) { + if (!is_array($this->data[$fieldName])) { + $result->addError(new Entity\EntityError('Связь должна быть множественным полем')); + + return $result; + } + // Извлечение данных для связи + $this->referencesData[$fieldName] = $this->data[$fieldName]; + unset($this->data[$fieldName]); + } + } + + return $result; + } + + /** + * Обработка данных для связей. + * + * @throws ArgumentException + */ + protected function processReferencesData() + { + /** + * @var DataManager $modelClass + */ + $modelClass = $this->modelClass; + $entity = $modelClass::getEntity(); + $fields = $entity->getFields(); + $result = new Entity\Result(); // пустой Result у которого isSuccess вернет true + + foreach ($this->referencesData as $fieldName => $referenceDataSet) { + if (!is_array($referenceDataSet)) { + continue; + } + + /** + * @var Entity\ReferenceField $reference + */ + $reference = $fields[$fieldName]; + $referenceDataSet = $this->linkDataSet($reference, $referenceDataSet); + $referenceStaleDataSet = $this->getReferenceDataSet($reference); + $fieldWidget = $this->getFieldWidget($fieldName); + + // Создание и обновление привязанных данных + $processedDataIds = array(); + foreach ($referenceDataSet as $referenceData) { + if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) { + // Создание связанных данных + if (!empty($referenceData[$fieldWidget->getMultipleField('VALUE')])) { + $result = $this->createReferenceData($reference, $referenceData); + + if ($result->isSuccess()) { + $processedDataIds[] = $result->getId(); + } else { + break; // ошибка, прерываем обработку данных + } + } + } else { + // Обновление связанных данных + $result = $this->updateReferenceData($reference, $referenceData, $referenceStaleDataSet); + + if ($result->isSuccess()) { + $processedDataIds[] = $referenceData[$fieldWidget->getMultipleField('ID')]; + } else { + break; // ошибка, прерываем обработку данных + } + } + } + + if($result->isSuccess()){ // Удаление записей, которые не были созданы или обновлены + foreach ($referenceStaleDataSet as $referenceData) { + if (!in_array($referenceData[$fieldWidget->getMultipleField('ID')], $processedDataIds)) { + $result = $this->deleteReferenceData($reference, + $referenceData[$fieldWidget->getMultipleField('ID')]); + if(!$result->isSuccess()) { + break; // ошибка, прерываем удаление данных + } + } + } + } + } + + $this->referencesData = array(); + return $result; + } + + /** + * Удаление данных всех связей, которые указаны в полях интерфейса раздела. + */ + protected function deleteReferencesData() + { + $references = $this->getReferences(); + $fields = $this->helper->getFields(); + $result = new Entity\Result(); + /** + * @var string $fieldName + * @var Entity\ReferenceField $reference + */ + foreach ($references as $fieldName => $reference) { + // Удаляются только данные связей, которые объявлены в интерфейсе + if (!isset($fields[$fieldName])) { + continue; + } + + $fieldWidget = $this->getFieldWidget($reference->getName()); + $referenceStaleDataSet = $this->getReferenceDataSet($reference); + + foreach ($referenceStaleDataSet as $referenceData) { + $result = $this->deleteReferenceData($reference, $referenceData[$fieldWidget->getMultipleField('ID')]); + if(!$result->isSuccess()){ + return $result; + } + } + } + return $result; + } + + /** + * Создание связанной записи. + * + * @param Entity\ReferenceField $reference + * @param array $referenceData + * + * @return \Bitrix\Main\Entity\AddResult + * @throws ArgumentException + */ + protected function createReferenceData(Entity\ReferenceField $reference, array $referenceData) + { + $referenceName = $reference->getName(); + $fieldParams = $this->getFieldParams($referenceName); + $fieldWidget = $this->getFieldWidget($referenceName); + + if (!empty($referenceData[$fieldWidget->getMultipleField('ID')])) { + throw new ArgumentException('Аргумент data не может содержать идентификатор элемента', 'data'); + } + + $refClass = $reference->getRefEntity()->getDataClass(); + + $createResult = $refClass::add($referenceData); + + if (!$createResult->isSuccess()) { + $this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR', + array('#FIELD#' => $fieldParams['TITLE'])), 'CREATE_' . $referenceName); + } + + return $createResult; + } + + /** + * Обновление связанной записи + * + * @param Entity\ReferenceField $reference + * @param array $referenceData + * @param array $referenceStaleDataSet + * + * @return Entity\UpdateResult|null + * @throws ArgumentException + */ + protected function updateReferenceData( + Entity\ReferenceField $reference, + array $referenceData, + array $referenceStaleDataSet + ) + { + $referenceName = $reference->getName(); + $fieldParams = $this->getFieldParams($referenceName); + $fieldWidget = $this->getFieldWidget($referenceName); + + if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) { + throw new ArgumentException('Аргумент data должен содержать идентификатор элемента', 'data'); + } + + // Сравнение старых данных и новых, обновляется только при различиях + if ($this->isDifferentData($referenceStaleDataSet[$referenceData[$fieldWidget->getMultipleField('ID')]], + $referenceData) + ) { + $refClass = $reference->getRefEntity()->getDataClass(); + $updateResult = $refClass::update($referenceData[$fieldWidget->getMultipleField('ID')], $referenceData); + + if (!$updateResult->isSuccess()) { + $this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR', + array('#FIELD#' => $fieldParams['TITLE'])), 'UPDATE_' . $referenceName); + } + + return $updateResult; + } else { + return new Entity\Result(); // пустой Result у которого isSuccess() вернет true + } + } + + /** + * Удаление данных связи. + * + * @param Entity\ReferenceField $reference + * @param $referenceId + * + * @return \Bitrix\Main\Entity\Result + * @throws ArgumentException + */ + protected function deleteReferenceData(Entity\ReferenceField $reference, $referenceId) + { + $fieldParams = $this->getFieldParams($reference->getName()); + $refClass = $reference->getRefEntity()->getDataClass(); + $deleteResult = $refClass::delete($referenceId); + + if (!$deleteResult->isSuccess()) { + $this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_DELETE_ERROR', + array('#FIELD#' => $fieldParams['TITLE'])), 'DELETE_' . $reference->getName()); + } + + return $deleteResult; + } + + /** + * Получение данных связи. + * + * @param $reference + * + * @return array + */ + protected function getReferenceDataSet(Entity\ReferenceField $reference) + { + /** + * @var DataManager $modelClass + */ + $modelClass = $this->modelClass; + $dataSet = array(); + $fieldWidget = $this->getFieldWidget($reference->getName()); + + $rsData = $modelClass::getList(array( + 'select' => array('REF_' => $reference->getName() . '.*'), + 'filter' => array('=' . $this->modelPk => $this->itemId) + )); + + while ($data = $rsData->fetch()) { + if (empty($data['REF_' . $fieldWidget->getMultipleField('ID')])) { + continue; + } + + $row = array(); + foreach ($data as $key => $value) { + $row[str_replace('REF_', '', $key)] = $value; + } + + $dataSet[$data['REF_' . $fieldWidget->getMultipleField('ID')]] = $row; + } + + return $dataSet; + } + + /** + * В данные связи подставляются данные основной модели используя условия связи моделей из getMap(). + * + * @param Entity\ReferenceField $reference + * @param array $referenceData Данные привязанной модели + * + * @return array + */ + protected function linkData(Entity\ReferenceField $reference, array $referenceData) + { + // Парсим условия связи двух моделей + $referenceConditions = $this->getReferenceConditions($reference); + + foreach ($referenceConditions as $refField => $refValue) { + // Так как в условиях связи между моделями в основном отношения типа this.field => ref.field или + // ref.field => SqlExpression, мы можем использовать это для подстановки данных + // this.field - это поле основной модели + // ref.field - поле модели из связи + // customValue - это строка полученная из new SqlExpression('%s', ...) + if (empty($refValue['thisField'])) { + $referenceData[$refField] = $refValue['customValue']; + } + else { + $referenceData[$refField] = $this->data[$refValue['thisField']]; + } + } + + return $referenceData; + } + + /** + * Связывает набор связанных данных с основной моделю. + * + * @param Entity\ReferenceField $reference + * @param array $referenceDataSet + * + * @return array + */ + protected function linkDataSet(Entity\ReferenceField $reference, array $referenceDataSet) + { + foreach ($referenceDataSet as $key => $referenceData) { + $referenceDataSet[$key] = $this->linkData($reference, $referenceData); + } + + return $referenceDataSet; + } + + /** + * Парсинг условий связи между моделями. + * + * Ничего сложного нет, просто определяются соответствия полей основной модели и модели из связи. Например: + * + * `FilmLinksTable::FILM_ID => FilmTable::ID (ref.FILM_ID => this.ID)` + * + * Или, например: + * + * `MediaTable::TYPE => 'FILM' (ref.TYPE => new DB\SqlExpression('?s', 'FILM'))` + * + * @param Entity\ReferenceField $reference Данные поля из getMap(). + * + * @return array Условия связи преобразованные в массив вида $conditions[$refField]['thisField' => $thisField, + * 'customValue' => $customValue]. + * $customValue - это результат парсинга SqlExpression. + * Если шаблон SqlExpression не равен %s, то условие исключается из результата. + */ + protected function getReferenceConditions(Entity\ReferenceField $reference) + { + $conditionsFields = array(); + + foreach ($reference->getReference() as $leftCondition => $rightCondition) { + $thisField = null; + $refField = null; + $customValue = null; + + // Поиск this.... в левом условии + $thisFieldMatch = array(); + $refFieldMatch = array(); + if (preg_match('/=this\.([A-z]+)/', $leftCondition, $thisFieldMatch) == 1) { + $thisField = $thisFieldMatch[1]; + } // Поиск ref.... в левом условии + else { + if (preg_match('/ref\.([A-z]+)/', $leftCondition, $refFieldMatch) == 1) { + $refField = $refFieldMatch[1]; + } + } + + // Поиск expression value... в правом условии + $refFieldMatch = array(); + if ($rightCondition instanceof \Bitrix\Main\DB\SqlExpression) { + $customValueDirty = $rightCondition->compile(); + $customValue = preg_replace('/^([\'"])(.+)\1$/', '$2', $customValueDirty); + if ($customValueDirty == $customValue) { + // Если значение выражения не обрамлено кавычками, значит оно не нужно нам + $customValue = null; + } + } // Поиск ref.... в правом условии + else { + if (preg_match('/ref\.([A-z]+)/', $rightCondition, $refFieldMatch) > 0) { + $refField = $refFieldMatch[1]; + } + } + + // Если не указано поле, которое нужно заполнить или не найдено содержимое для него, то исключаем условие + if (empty($refField) || (empty($thisField) && empty($customValue))) { + continue; + } + else { + $conditionsFields[$refField] = array( + 'thisField' => $thisField, + 'customValue' => $customValue, + ); + } + } + + return $conditionsFields; + } + + /** + * Обнаружение отличий массивов + * Метод не сранивает наличие аргументов, сравниваются только значения общих параметров + * + * @param array $data1 + * @param array $data2 + * + * @return bool + */ + protected function isDifferentData(array $data1 = null, array $data2 = null) + { + foreach ($data1 as $key => $value) { + if (isset($data2[$key]) && $data2[$key] != $value) { + return true; + } + } + + return false; + } + + /** + * @param $fieldName + * + * @return array|bool + */ + protected function getFieldParams($fieldName) + { + $fields = $this->helper->getFields(); + + if (isset($fields[$fieldName]) && isset($fields[$fieldName]['WIDGET'])) { + return $fields[$fieldName]; + } + else { + return false; + } + } + + /** + * Получение виджета привязанного к полю. + * + * @param $fieldName + * + * @return HelperWidget|bool + */ + protected function getFieldWidget($fieldName) + { + $field = $this->getFieldParams($fieldName); + + return isset($field['WIDGET']) ? $field['WIDGET'] : null; + } +} \ No newline at end of file diff --git a/lib/EventHandlers.php b/lib/EventHandlers.php index bbb5ecf..7612ecf 100644 --- a/lib/EventHandlers.php +++ b/lib/EventHandlers.php @@ -5,7 +5,6 @@ use Bitrix\Main\Context; use Bitrix\Main\Loader; -include_once("Loader.php"); /** * Перехватчики событий. * diff --git a/lib/Loader.php b/lib/Loader.php deleted file mode 100644 index 2afbd60..0000000 --- a/lib/Loader.php +++ /dev/null @@ -1,36 +0,0 @@ -getRequest()->isAdminSection()) { - if (class_exists($interface)) { - $interface::register(); - } else { - require_once $interface; - } - } - } -} \ No newline at end of file diff --git a/lib/helper/AdminBaseHelper.php b/lib/helper/AdminBaseHelper.php index 8b86ace..82de0c8 100644 --- a/lib/helper/AdminBaseHelper.php +++ b/lib/helper/AdminBaseHelper.php @@ -1,712 +1,1118 @@ - - *
  • Мдель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.
  • - *
  • Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.
  • - *
  • Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные - * хэлперы с нужными настройками. С ним напрямую работать не придётся.
  • - *
  • Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей - * сущностей. В списке и на детальной.
  • - * - * - * Схема работы с модулем следующя: - * - * - * Рекомендуемая файловая структура для модулей, использующих данный функционал: - * - * - * Использовать данную структуру не обязательно, это лишь рекомендация, основанная на успешном опыте применения модуля - * в ряде проектов. - * - * @see AdminBaseHelper::setInterfaceSettings() - * @package AdminHelper - * @FIXME: Упростить обработку сообщений об ошибках: слишком запутанно. - */ -abstract class AdminBaseHelper -{ - /** - * @internal - * @var string адрес обработчика запросов к админ. интерфейсу. - */ - static protected $routerUrl = '/bitrix/admin/admin_helper_route.php'; - - /** - * @var string - * Имя класса используемой модели. Используется для выполнения CRUD-операций. - * При наследовании класса необходимо переопределить эту переменную, указав полное имя класса модели. - * - * @see DataManager - * @api - */ - static protected $model; - - /** - * @var string - * Назвние модуля данной модели. - * При наследовании класса необходимо указать нзвание модуля, в котором он находится. - * Используется для избежания конфликтов между именами представлений. - * - * @api - */ - static public $module = ''; - - /** - * @var string - * Название представления. - * При наследовании класса необходимо указать название представления. Оно будет использовано при построении URL к - * данному разделу админки. Не должно содержать пробелов и других символов, требующих преобразований для - * адресной строки браузера. - * - * @api - */ - static protected $viewName; - - /** - * @var array - * Настройки интерфейса - * @see AdminBaseHelper::setInterfaceSettings() - * @internal - */ - static protected $interfaceSettings = array(); - - /** - * @var array - * Хранит список отображаемых полей и настройки их отображения - * @see AdminBaseHelper::setInterfaceSettings() - */ - protected $fields = array(); - - /** - * @var \CMain - * Замена global $APPLICATION; - */ - protected $app; - protected $validationErrors = array(); - - /** - * @var string - * Позволяет непосредственно указать адрес страницы списка. Полезно, в случае, если такая станица реализована без - * использования данного модуля. В случае, если поле определено для класса, роутинг не используется. - * - * @see AdminBaseHelper::getListPageUrl - * @api - */ - static protected $listPageUrl; - - /** - * @var string - * $viewName представления, отвечающего за страницу списка. Необходимо указывать только для классов, уналедованных - * от AdminEditHelper. - * - * @see AdminBaseHelper::$viewName - * @see AdminBaseHelper::getListPageUrl - * @see AdminEditHelper - * @api - */ - static protected $listViewName; - - /** - * @var string - * Позволяет непосредственно указать адрес страницы просмотра/редактирования элемента. Полезно, в случае, если - * такая станица реализована без использования данного модуля. В случае, если поле определено для класса, - * роутинг не используется. - * - * @see AdminBaseHelper::getEditPageUrl - * @api - */ - static protected $editPageUrl; - - - /** - * @var string - * $viewName представления, отвечающего за страницу редактирования/просмотра элемента. Необходимо указывать только - * для классов, уналедованных от AdminListHelper. - * - * @see AdminBaseHelper::$viewName - * @see AdminBaseHelper::getEditPageUrl - * @see AdminListHelper - * @api - */ - static protected $editViewName; - - /** - * @var array - * Дополнительные параметры URL, которые будут добавлены к параметрам по-умолчанию, генерируемым автоматически - * @api - */ - protected $additionalUrlParams = array(); - - /** - * @var string контекст выполнения. Полезен для информирования виджетов о том, какая операция в настоящий момент - * производится. - */ - protected $context = ''; - - /** - * @param array $fields список используемых полей и виджетов для них - * @param array $tabs список вкладок для детальной страницы - * @param string $module название модуля - */ - public function __construct(array $fields, array $tabs = array(), $module = "") - { - global $APPLICATION; - $this->app = $APPLICATION; - - $this->loadMessages(); - - $settings = array( - 'FIELDS' => $fields, - 'TABS' => $tabs - ); - if (static::setInterfaceSettings($settings)) { - $this->fields = $fields; - } else { - $settings = static::getInterfaceSettings(); - $this->fields = $settings['FIELDS']; - } - } - - /** - * Подгрузка ленг-файла с сообщениями для локализации - */ - protected function loadMessages() - { - - } - - /** - * @param string $viewName - имя вьюхи, для которой мы хотим получить натсройки - * - * @return array Возвращает настройки интерфейса для данного класса. - * - * @see AdminBaseHelper::setInterfaceSettings() - * @api - */ - static public function getInterfaceSettings($viewName = '') - { - if (empty($viewName)) { - $viewName = static::$viewName; - } - return self::$interfaceSettings[static::getModule()][$viewName]['interface']; - } - - /** - * Основная функция для конфигурации всего административного интерфейса. - * - * @param array $settings настройки полей и вкладок - * @param array $helpers список классов-хэлперов, используемых для отрисовки админки - * @param string $module название модуля - * - * @return bool false, если для данного класса уже были утановлены настройки - * - * @api - */ - static public function setInterfaceSettings(array $settings, array $helpers = array(), $module = '') - { - foreach ($helpers as $helper/**@var AdminBaseHelper $helper */) { - $success = $helper::registerInterfaceSettings($module, $settings); - if (!$success) return false; - } - - return true; - } - - /** - * Регистрирует настройки интерфейса для текущего хелпера - * - * @param string $module имя текущего модуля - * @param $interfaceSettings - * @return bool - * @internal - */ - static public function registerInterfaceSettings($module, $interfaceSettings) - { - if (empty($module)) { - return false; - } - self::$module = $module; - - if (empty($interfaceSettings)) { - return false; - } - - if (isset(self::$interfaceSettings[$module][static::$viewName])) { - return false; - } - - self::$interfaceSettings[$module][static::$viewName] = array( - 'helper' => get_called_class(), - 'interface' => $interfaceSettings - ); - - return true; - } - - /** - * Получает настройки интерфейса для данного модуля и представления - * Используется при роутинге. - * Возвращается массив со следующими ключами: - * - * - * - * @param string $module Модуль, для которого нужно получить настройки - * @param string $view Название представления - * @return array - * @internal - */ - static public function getGlobalInterfaceSettings($module, $view) - { - if (!isset(self::$interfaceSettings[$module][$view])) { - return false; - } - - return array( - self::$interfaceSettings[$module][$view]['helper'], - self::$interfaceSettings[$module][$view]['interface'], - ); - } - - /** - * @return string - * Возвращает имя текущего представления - * @api - */ - public static function getViewName() - { - return static::$viewName; - } - - /** - * @return \Bitrix\Main\Entity\DataManager|string Возвращает имя класса используемой модели - * Возвращает имя класса используемой модели - * - * @throws \Bitrix\Main\ArgumentException - * @throws \Bitrix\Main\SystemException - * @throws \Exception - * @api - */ - public static function getModel() - { - return static::getHLEntity(static::$model); - } - - /** - * Возвращает имя модуля - * @return string - * @api - */ - static public function getModule() - { - return static::$module; - } - - /** - * Возвращает список полей, переданных через AdminBaseHelper::setInterfaceSettings() - * @see AdminBaseHelper::setInterfaceSettings() - * @return array - * @api - */ - public function getFields() - { - return $this->fields; - } - - /** - * Окончательно выводит админисстративную страницу - * @internal - */ - abstract public function show(); - - /** - * Получает название таблицы используемой модели - * @return mixed - */ - public function table() - { - /**@var DataManager $className */ - $className = static::getModel(); - - return $className::getTableName(); - } - - /** - * Возвращает первичный ключ таблицы используемой модели - * Для HL-инфоблоков битрикс - всегда ID. Но может поменяться для какой-либо другой сущности. - * @return string - * @api - */ - public function pk() - { - return 'ID'; - } - - /** - * Устанавливает заголовок раздела в админке - * @param $title - * @api - */ - public function setTitle($title) - { - $this->app->SetTitle($title); - } - - /** - * Функция для обработки дополнительных операций над элементами в админке. - * Как правило должно оканчиваться LocalRedirect после внесения изменений. - * - * @param string $action Название действия - * @param null|int $id ID элемента - * @api - */ - protected function customActions($action, $id = null) - { - return; - } - - /** - * Выполняется проверка прав на выполнение опреаций редактирования элементов - * @return bool - * @api - */ - protected function hasRights() - { - return true; - } - - /** - * Выводит сообщения об ошибках - * @internal - */ - protected function showMessages() - { - $allErrors = $this->getErrors(); - $notes = $this->getNotes(); - - if (!empty($allErrors)) { - $errorList[] = implode("\n", $allErrors); - } - if ($e = $this->getLastException()) { - $errorList[] = trim($e->GetString()); - } - - - if (!empty($errorList)) { - $errorText = implode("\n\n", $errorList); - \CAdminMessage::ShowOldStyleError($errorText); - - } else { - if (!empty($notes)) { - $noteText = implode("\n\n", $notes); - \CAdminMessage::ShowNote($noteText); - } - } - } - - /** - * @return bool|\CApplicationException - * @internal - */ - protected function getLastException() - { - if (isset($_SESSION['APPLICATION_EXCEPTION']) AND !empty($_SESSION['APPLICATION_EXCEPTION'])) { - /** @var CApplicationException $e */ - $e = $_SESSION['APPLICATION_EXCEPTION']; - unset($_SESSION['APPLICATION_EXCEPTION']); - - return $e; - } else { - return false; - } - - } - - /** - * @param $e - */ - protected function setAppException($e) - { - $_SESSION['APPLICATION_EXCEPTION'] = $e; - } - - /** - * Добавляет ошибку или массив ошибок для показа пользователю - * @param array|string $errors - * @api - */ - public function addErrors($errors) - { - if (!is_array($errors)) { - $errors = array($errors); - } - - if (isset($_SESSION['ELEMENT_SAVE_ERRORS']) AND !empty($_SESSION['ELEMENT_SAVE_ERRORS'])) { - $_SESSION['ELEMENT_SAVE_ERRORS'] = array_merge($_SESSION['ELEMENT_SAVE_ERRORS'], $errors); - } else { - $_SESSION['ELEMENT_SAVE_ERRORS'] = $errors; - } - } - - /** - * Добавляет уведомление или список уведомлений для показа пользователю - * @param array|string $notes - * @api - */ - public function addNotes($notes) - { - if (!is_array($notes)) { - $notes = array($notes); - } - - if (isset($_SESSION['ELEMENT_SAVE_NOTES']) AND !empty($_SESSION['ELEMENT_SAVE_NOTES'])) { - $_SESSION['ELEMENT_SAVE_NOTES'] = array_merge($_SESSION['ELEMENT_SAVE_NOTES'], - $notes); - } else { - $_SESSION['ELEMENT_SAVE_NOTES'] = $notes; - } - } - - /** - * @return bool|array - * @api - */ - protected function getErrors() - { - if (isset($_SESSION['ELEMENT_SAVE_ERRORS']) AND !empty($_SESSION['ELEMENT_SAVE_ERRORS'])) { - $errors = $_SESSION['ELEMENT_SAVE_ERRORS']; - unset($_SESSION['ELEMENT_SAVE_ERRORS']); - - return $errors; - } else { - return false; - } - } - - /** - * @return bool - * @api - */ - protected function getNotes() - { - if (isset($_SESSION['ELEMENT_SAVE_NOTES']) AND !empty($_SESSION['ELEMENT_SAVE_NOTES'])) { - $notes = $_SESSION['ELEMENT_SAVE_NOTES']; - unset($_SESSION['ELEMENT_SAVE_NOTES']); - - return $notes; - } else { - return false; - } - } - - /** - * Возвращает URL страницы редактирования класса данного представления - * @param array $params - * @return string - * @api - */ - static public function getEditPageURL($params = array()) - { - $viewName = isset(static::$editViewName) ? static::$editViewName : static::$viewName; - if (!isset($viewName)) { - $query = "?lang=" . LANGUAGE_ID . '&' . http_build_query($params); - if (is_subclass_of(get_called_class(), 'AdminEditHelper')) { - return $query; - } else { - return static::$editPageUrl . $query; - } - } - - return static::getViewURL($viewName, static::$editPageUrl, $params); - } - - - /** - * Возвращает URL страницы списка класса данного представления - * @param array $params - * @return string - * @api - */ - static public function getListPageURL($params = array()) - { - $viewName = isset(static::$listViewName) ? static::$listViewName : static::$viewName; - - return static::getViewURL($viewName, static::$listPageUrl, $params); - } - - /** - * Получает URL для указанного представления - * - * @param string $viewName название представления - * @param string $defaultURL позволяет указать URL напрямую. Если указано, то будет использовано это значение - * @param array $params - дополнительные query-параметры в URL - * @return string - * @internal - */ - static public function getViewURL($viewName, $defaultURL, $params = array()) - { - if (isset($defaultURL)) { - $url = $defaultURL . "?lang=" . LANGUAGE_ID; - } else { - $url = static::getRouterURL() . '?lang=' . LANGUAGE_ID . '&module=' . static::getModule() . '&view=' . $viewName; - } - - if (!empty($params)) { - unset($params['lang']); - unset($params['module']); - unset($params['view']); - - $query = http_build_query($params); - $url .= '&' . $query; - } - - return $url; - } - - /** - * Возвращает адрес обработчика запросов к админ. интерфейсу. - * @return string - * @api - */ - static public function getRouterURL() - { - return static::$routerUrl; - } - - /** - * Получает виджет для текущего поля, выполняет базовую инициализацию. - * - * @param string $code ключ поля для данного виджета (должен быть в массиве $data) - * @param array $data - данные объекта в виде массива - * @return bool|\DigitalWand\AdminHelper\Widget\HelperWidget - * @throws \DigitalWand\AdminHelper\Helper\Exception - * @internal - */ - public function createWidgetForField($code, &$data = array()) - { - if (!isset($this->fields[$code]['WIDGET'])) { - $error = str_replace('#CODE#', $code, 'Can\'t create widget for the code "#CODE#"'); - throw new Exception($error, Exception::CODE_NO_WIDGET); - } - - /** @var HelperWidget $widget */ - $widget = $this->fields[$code]['WIDGET']; - - $widget->setHelper($this); - $widget->setCode($code); - $widget->setEntityName($this->getModel()); - $widget->setData($data); - - return $widget; - } - - /** - * Если класс не объявлен, то битрикс генерирует новый класс в рантайме. - * Если класс уже есть, то возвращаем имя как есть. - * - * @param $className - * @return \Bitrix\Highloadblock\DataManager - * - * @throws \Bitrix\Main\ArgumentException - * @throws \Bitrix\Main\SystemException - * @throws Exception - * - */ - public static function getHLEntity($className) - { - if (!class_exists($className)) { - $info = static::getHLEntityInfo($className); - if ($info) { - $entity = HL\HighloadBlockTable::compileEntity($info); - return $entity->getDataClass(); - } else { - $error = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_GETMODEL_EXCEPTION', array('#CLASS#' => $className)); - $exception = new Exception($error, Exception::CODE_NO_HL_ENTITY_INFORMATION); - - throw $exception; - } - } - - return $className; - } - - /** - * Получает запись из БД с информацией об HL. - * - * @param $className - название класса, обязательно без Table в конце и без указания неймспейса - * @return array|false - * @throws \Bitrix\Main\ArgumentException - */ - public static function getHLEntityInfo($className) - { - $className = str_replace('\\', '', $className); - $pos = strripos($className, 'Table', -5); - if ($pos !== false) { - $className = substr($className, 0, $pos); - } - $parameters = array( - 'filter' => array( - 'NAME' => $className, - ), - 'limit' => 1 - ); - - return HL\HighloadBlockTable::getList($parameters)->fetch(); - } - - /** - * Выставляет текущий контекст исполнения. - * @param $context - * @see $context - */ - protected function setContext($context) - { - $this->context = $context; - } - - public function getContext() - { - return $this->context; - } + + *
  • Мдель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.
  • + *
  • Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.
  • + *
  • Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные + * хэлперы с нужными настройками. С ним напрямую работать не придётся.
  • + *
  • Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей + * сущностей. В списке и на детальной.
  • + * + * + * Схема работы с модулем следующая: + * + * + * Устаревший функционал: + * - * - * Этого будет дастаточно для получения минимальной функциональности - * - * @see AdminBaseHelper::$model - * @see AdminBaseHelper::$module - * @see AdminBaseHelper::$listViewName - * @see AdminBaseHelper::$viewName - * @package AdminHelper - */ -abstract class AdminEditHelper extends AdminBaseHelper -{ - - const OP_SHOW_TAB_ELEMENTS = 'AdminEditHelper::showTabElements'; - const OP_EDIT_ACTION_BEFORE = 'AdminEditHelper::editAction_before'; - const OP_EDIT_ACTION_AFTER = 'AdminEditHelper::editAction_after'; - - /** - * @var array - * Данные сущности, редактируемой в данный момент. - * Ключи ассива - названия полей в БД. - * @api - */ - protected $data; - - /** - * @var array - * Вкладки страницы редактирования - */ - protected $tabs = array(); - - /** - * @var array - * Элементы верхнего меню страницы - * @see AdminEditHelper::fillMenu() - */ - protected $menu = array(); - - /** - * @var \CAdminForm - */ - protected $tabControl; - - - /** - * Производится инициализация переменных, обработка запросов на редактирование - * - * @param array $fields - * @param array $tabs - * - * @see AdminBaseHelper::setInterfaceSettings() - */ - public function __construct(array $fields, array $tabs = array()) - { - $this->tabs = $tabs; - if (empty($this->tabs)) { - $this->tabs = array( - array( - 'DIV' => 'DEFAULT_TAB', - 'TAB' => Loc::getMessage('DEFAULT_TAB'), - "ICON" => "main_user_edit", - 'TITLE' => Loc::getMessage('DEFAULT_TAB'), - 'VISIBLE' => true, - ) - ); - } else { - if (!is_array(reset($this->tabs))) { - $converted = array(); - foreach ($this->tabs as $tabCode => $tabName) { - $tabVisible = true; - if (is_array($tabName)) { - $tabVisible = isset($tabName['VISIBLE']) ? $tabName['VISIBLE'] : $tabVisible; - $tabName = $tabName['TITLE']; - } - $converted[] = array( - 'DIV' => $tabCode, - 'TAB' => $tabName, - 'ICON' => '', - 'TITLE' => $tabName, - 'VISIBLE' => $tabVisible, - ); - } - $this->tabs = $converted; - } - } - - parent::__construct($fields, $tabs); - - $this->tabControl = new \CAdminForm(str_replace("\\", "", get_called_class()), $this->tabs); - - if (isset($_REQUEST['apply']) OR isset($_REQUEST['save'])) { - - $this->data = $_REQUEST['FIELDS']; - if (isset($_REQUEST[$this->pk()])) { - //Первичный ключ проставляем отдельно, чтобы не вынуждать всегда указывать его в настройках интерфейса. - $this->data[$this->pk()] = $_REQUEST[$this->pk()]; - } - - foreach ($fields as $code => $settings) { - if(isset($_REQUEST[$code])){ - $this->data[$code] = $_REQUEST[$code]; - } - } - - if ($this->editAction()) { - if (isset($_REQUEST['apply'])) { - $id = $this->data[$this->pk()]; - $url = $this->app->GetCurPageParam($this->pk() . '=' . $id); - - } else { - if (isset($_REQUEST['save'])) { - $url = $this->getListPageURL(array_merge($this->additionalUrlParams, - array( - 'restore_query' => 'Y' - ))); - } - } - - } else { - if (isset($this->data[$this->pk()])) { - $id = $this->data[$this->pk()]; - $url = $this->app->GetCurPageParam($this->pk() . '=' . $id); - } else { - unset($this->data); - $this->data = $_REQUEST['FIELDS']; //Заполняем, чтобы в случае ошибки сохранения поля не были пустыми - } - } - - if (isset($url)) { - $this->setAppException($this->app->GetException()); - LocalRedirect($url); - } - - } else { - $helperFields = $this->getFields(); - $select = array_keys($helperFields); - - foreach ($select as $key => $field) { - - if (isset($helperFields[$field]['VIRTUAL']) - AND $helperFields[$field]['VIRTUAL'] == true - AND (!isset($helperFields[$field]['FORCE_SELECT']) OR $helperFields[$field]['FORCE_SELECT'] = false) - ) { - unset($select[$key]); - } - } - - $this->data = $this->loadElement($select); - if (!$this->data) { - //TODO: элемент не найден - } - - if (isset($_REQUEST['action'])) { - $this->customActions($_REQUEST['action'], - $this->data[$this->pk()]); - } - } - - $this->setElementTitle(); - } - - /** - * Заполняет верхнее меню страницы - * По-умолчанию добавляет две кнопки: - * - * - * Добавляя новые кнопки, нужно указывать параметр URl "action", который будет обрабатываться в - * AdminEditHelper::customActions() - * - * @param bool $showDeleteButton управляет видимостью кнопки удаления элемента - * @see AdminEditHelper::$menu - * @see AdminEditHelper::customActions() - * @api - */ - protected function fillMenu($showDeleteButton = true) - { - $returnToList = array( - "TEXT" => Loc::getMessage('RETURN_TO_LIST'), - "TITLE" => Loc::getMessage('RETURN_TO_LIST'), - "LINK" => $this->getListPageURL(array_merge($this->additionalUrlParams, - array( - 'restore_query' => 'Y' - ))), - "ICON" => "btn_list", - ); - - if (!empty($this->menu)) { - array_unshift($this->menu, $returnToList); - } else { - $this->menu[] = $returnToList; - } - - if ($showDeleteButton && isset($this->data[$this->pk()]) && $this->hasRights()) { - $this->menu[] = array( - "TEXT" => Loc::getMessage('DELETE'), - "TITLE" => Loc::getMessage('DELETE'), - "LINK" => static::getEditPageURL(array_merge($this->additionalUrlParams, - array( - 'ID' => $this->data[$this->pk()], - 'action' => 'delete', - 'lang' => LANGUAGE_ID, - 'restore_query' => 'Y', - ))), - ); - } - } - - /** - * Выводит детальную страницу - * @internal - */ - public function show() - { - $this->fillMenu(); - $context = new \CAdminContextMenu($this->menu); - $context->Show(); - - $this->tabControl->BeginPrologContent(); - $this->showMessages(); - $this->showProlog(); - $this->tabControl->EndPrologContent(); - - $this->tabControl->BeginEpilogContent(); - $this->showEpilog(); - $this->tabControl->EndEpilogContent(); - - - $query = $this->additionalUrlParams; - if (isset($_REQUEST[$this->pk()])) { - $query[$this->pk()] = $_REQUEST[$this->pk()]; - } - - $this->tabControl->Begin(array( - 'FORM_ACTION' => static::getEditPageURL($query) - )); - - foreach ($this->tabs as $tabSettings) { - if ($tabSettings['VISIBLE']) { - $this->showTabElements($tabSettings); - } - } - - $this->tabControl->Buttons(array( - "back_url" => $this->getListPageURL(array_merge($this->additionalUrlParams, - array( - 'lang' => LANGUAGE_ID, - 'restore_query' => 'Y', - ))) - )); - $this->tabControl->ShowWarnings('editform', array()); //TODO: дописать - $this->tabControl->Show(); - } - - /** - * Отрисовка верхней части страницы. - * @api - */ - protected function showProlog() - { - - } - - /** - * Отрисовка нижней части страницы. - * По-умолчанию рисует все поля, которые не попали в вывод, как input hidden - * @api - */ - protected function showEpilog() - { - $interfaceSettings = static::getInterfaceSettings(); - foreach ($interfaceSettings['FIELDS'] as $code => $settings) { - if (!isset($settings['TAB']) AND - isset($settings['FORCE_SELECT']) AND - $settings['FORCE_SELECT'] == true - ) { - - print ''; - } - } - } - - /** - * Отрисовывает вкладку со всеми привязанными к ней полями. - * - * @param $tabSettings - * @internal - */ - protected function showTabElements($tabSettings) - { - $this->setContext(AdminEditHelper::OP_SHOW_TAB_ELEMENTS); - - $this->tabControl->BeginNextFormTab(); - foreach ($this->getFields() as $code => $fieldSettings) { - - $widget = $this->createWidgetForField($code, $this->data); - - $fieldTab = $widget->getSettings('TAB'); - $fieldOnCurrentTab = ($fieldTab == $tabSettings['DIV'] OR $tabSettings['DIV'] == 'DEFAULT_TAB'); - - if (!$fieldOnCurrentTab) { - continue; - } - - $fieldSettings = $widget->getSettings(); - if (isset($fieldSettings['VISIBLE']) && $fieldSettings['VISIBLE'] === false) { - continue; - } - - $this->tabControl->BeginCustomField($code, $widget->getSettings('TITLE')); - $pkField = ($code == $this->pk()); - $widget->genBasicEditField($pkField); - $this->tabControl->EndCustomField($code); - - } - } - - /** - * Обработка запроса редактирования страницы - * Этапы: - * - * - * @return bool - * @see HelperWidget::processEditAction(); - * @see HelperWidget::processAfterSaveAction(); - * @internal - */ - protected function editAction() - { - $this->setContext(AdminEditHelper::OP_EDIT_ACTION_BEFORE); - if (!$this->hasRights()) { - $this->addErrors('Недостаточно прав для редактирования данных'); - - return false; - } - $allWidgets = array(); - foreach ($this->getFields() as $code => $settings) { - $widget = $this->createWidgetForField($code, $this->data); - $widget->processEditAction(); - $this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors()); - $allWidgets[] = $widget; - } - - $this->addErrors($this->validationErrors); - - $success = empty($this->validationErrors); - if ($success) { - - $this->setContext(AdminEditHelper::OP_EDIT_ACTION_AFTER); - - $existing = false; - $id = isset($_REQUEST['FIELDS'][$this->pk()]) ? $_REQUEST['FIELDS'][$this->pk()] : $_REQUEST[$this->pk()]; - if ($id) { - - /** @var DataManager $className */ - $className = static::getModel(); - // Если имеется primary key, то модель уже существующая, пытаемся найти ее в БД - $existing = $className::getById($id)->fetch(); - - } - if ($existing) { - $result = $this->saveElement($id); - } else { - $result = $this->saveElement(); - } - - if (!$result->isSuccess()) { - $this->addErrors($result->getErrorMessages()); - return false; - } - foreach ($allWidgets as $widget) { - /** @var HelperWidget $widget */ - $widget->setData($this->data); - $widget->processAfterSaveAction(); - } - - return true; - } - - return false; - } - - /** - * Функция загрузки элемента из БД. - * Можно переопределить, если требуется сложная логика и нет возможности определить её в модели. - * - * @param array $select - * - * @return bool - * @api - */ - protected function loadElement($select = array()) - { - if (isset($_REQUEST[$this->pk()])) { - $className = static::getModel(); - $result = $className::getById($_REQUEST[$this->pk()]); - - return $result->fetch(); - } - - return false; - } - - /** - * Сохранение элемента. - * Можно переопределить, если требуется сложная логика и нет возможности определить её в модели. - * - * @param bool $id - * @return \Bitrix\Main\Entity\AddResult|\Bitrix\Main\Entity\UpdateResult - * @throws \Exception - * @api - */ - protected function saveElement($id = false) - { - $className = static::getModel(); - - if ($id) { - $result = $className::update($id, $this->data); - } else { - $result = $className::add($this->data); - } - - return $result; - } - - /** - * Удаление элемента. - * Можно переопределить, если требуется сложная логика и нет возможности определить её в модели. - * - * @param $id - * @return \Bitrix\Main\Entity\DeleteResult - * @throws \Exception - * @api - */ - protected function deleteElement($id) - { - $className = static::getModel(); - $result = $className::delete($id); - - return $result; - } - - /** - * Выполнение кастомных операций над объектом в процессе редактирования - * - * @param string $action название операции - * @param int|null $id ID элемента - * @see AdminEditHelper::fillMenu() - * @api - */ - protected function customActions($action, $id) - { - if ($action == 'delete' AND !is_null($id)) { - $this->deleteElement($id); - - LocalRedirect($this->getListPageURL(array_merge($this->additionalUrlParams, - array( - 'restore_query' => 'Y' - )))); - } - } - - /** - * Устанавливает заголовок исходя из данных текущего элемента - * - * @see $data - * @see AdminBaseHelper::setTitle() - * @api - */ - protected function setElementTitle() - { - if (!empty($this->data)) { - $title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_TITLE', array('#ID#' => $this->data[$this->pk()])); - } else { - $title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_NEW_ELEMENT'); - } - - $this->setTitle($title); - } - - /** - * @return \CAdminForm - */ - public function getTabControl() - { - return $this->tabControl; - } - -} - + + *
  • static protected $model
  • + * + * + * Этого будет дастаточно для получения минимальной функциональности. + * + * @package AdminHelper + * + * @see AdminBaseHelper::$model + * @see AdminBaseHelper::$module + * @see AdminBaseHelper::$listViewName + * @see AdminBaseHelper::$viewName + * + * @author Nik Samokhvalov + * @author Artem Yarygin + */ +abstract class AdminEditHelper extends AdminBaseHelper +{ + const OP_SHOW_TAB_ELEMENTS = 'AdminEditHelper::showTabElements'; + const OP_EDIT_ACTION_BEFORE = 'AdminEditHelper::editAction_before'; + const OP_EDIT_ACTION_AFTER = 'AdminEditHelper::editAction_after'; + + /** + * @var array Данные сущности, редактируемой в данный момент. Ключи ассива — названия полей в БД. + * @api + */ + protected $data; + /** + * @var array Вкладки страницы редактирования. + */ + protected $tabs = array(); + /** + * @var array Элементы верхнего меню страницы. + * @see AdminEditHelper::fillMenu() + */ + protected $menu = array(); + /** + * @var \CAdminForm + */ + protected $tabControl; + + /** + * Производится инициализация переменных, обработка запросов на редактирование + * + * @param array $fields + * @param array $tabs + */ + public function __construct(array $fields, array $tabs = array()) + { + $this->tabs = $tabs; + + if (empty($this->tabs)) { + $this->tabs = array( + array( + 'DIV' => 'DEFAULT_TAB', + 'TAB' => Loc::getMessage('DEFAULT_TAB'), + 'ICON' => 'main_user_edit', + 'TITLE' => Loc::getMessage('DEFAULT_TAB'), + 'VISIBLE' => true, + ) + ); + } + else { + if (!is_array(reset($this->tabs))) { + $converted = array(); + + foreach ($this->tabs as $tabCode => $tabName) { + $tabVisible = true; + + if (is_array($tabName)) { + $tabVisible = isset($tabName['VISIBLE']) ? $tabName['VISIBLE'] : $tabVisible; + $tabName = $tabName['TITLE']; + } + + $converted[] = array( + 'DIV' => $tabCode, + 'TAB' => $tabName, + 'ICON' => '', + 'TITLE' => $tabName, + 'VISIBLE' => $tabVisible, + ); + } + $this->tabs = $converted; + } + } + + parent::__construct($fields, $tabs); + + $this->tabControl = new \CAdminForm(str_replace("\\", "", get_called_class()), $this->tabs); + + if (isset($_REQUEST['apply']) OR isset($_REQUEST['save'])) { + $this->data = $_REQUEST['FIELDS']; + + if (isset($_REQUEST[$this->pk()])) { + //Первичный ключ проставляем отдельно, чтобы не вынуждать всегда указывать его в настройках интерфейса. + $this->data[$this->pk()] = $_REQUEST[$this->pk()]; + } + + foreach ($fields as $code => $settings) { + if (isset($_REQUEST[$code])) { + $this->data[$code] = $_REQUEST[$code]; + } + } + + if ($this->editAction()) { + if (isset($_REQUEST['apply'])) { + $id = $this->data[$this->pk()]; + $url = $this->app->GetCurPageParam($this->pk() . '=' . $id); + } + else { + if (isset($_REQUEST['save'])) { + $listHelperClass = static::getHelperClass(AdminListHelper::className()); + $url = $listHelperClass::getUrl(array_merge($this->additionalUrlParams, + array( + 'restore_query' => 'Y' + ))); + } + } + } + else { + if (isset($this->data[$this->pk()])) { + $id = $this->data[$this->pk()]; + $url = $this->app->GetCurPageParam($this->pk() . '=' . $id); + } + else { + unset($this->data); + $this->data = $_REQUEST['FIELDS']; //Заполняем, чтобы в случае ошибки сохранения поля не были пустыми + } + } + + if (isset($url)) { + $this->setAppException($this->app->GetException()); + LocalRedirect($url); + } + } + else { + $helperFields = $this->getFields(); + $select = array_keys($helperFields); + + foreach ($select as $key => $field) { + if (isset($helperFields[$field]['VIRTUAL']) + AND $helperFields[$field]['VIRTUAL'] == true + AND (!isset($helperFields[$field]['FORCE_SELECT']) OR $helperFields[$field]['FORCE_SELECT'] = false) + ) { + unset($select[$key]); + } + } + + $this->data = $this->loadElement($select); + + if (!$this->data) { + //TODO: элемент не найден + } + + if (isset($_REQUEST['action']) || isset($_REQUEST['action_button'])) { + $id = isset($_REQUEST['ID']) ? $_REQUEST['ID'] : null; + $action = isset($_REQUEST['action']) ? $_REQUEST['action'] : $_REQUEST['action_button']; + $this->customActions($action, $id); + } + } + + $this->setElementTitle(); + } + + /** + * Возвращает верхнее меню страницы. + * По-умолчанию две кнопки: + *
      + *
    • Возврат в список
    • + *
    • Удаление элемента
    • + *
    + * + * Добавляя новые кнопки, нужно указывать параметр URl "action", который будет обрабатываться в + * AdminEditHelper::customActions() + * + * @param bool $showDeleteButton Управляет видимостью кнопки удаления элемента. + * + * @return array + * + * @see AdminEditHelper::$menu + * @see AdminEditHelper::customActions() + * + * @api + */ + protected function getMenu($showDeleteButton = true) + { + $listHelper = static::getHelperClass(AdminListHelper::className()); + + $menu = array( + $this->getButton('RETURN_TO_LIST', array( + 'LINK' => $listHelper::getUrl(array_merge($this->additionalUrlParams, + array('restore_query' => 'Y') + )), + 'ICON' => 'btn_list', + )) + ); + + $arSubMenu = array(); + + if (isset($this->data[$this->pk()]) && $this->hasWriteRights()) { + $arSubMenu[] = $this->getButton('ADD_ELEMENT', array( + 'LINK' => static::getUrl(array_merge($this->additionalUrlParams, + array( + 'action' => 'add', + 'lang' => LANGUAGE_ID, + 'restore_query' => 'Y', + ))), + 'ICON' => 'edit' + )); + } + + if ($showDeleteButton && isset($this->data[$this->pk()]) && $this->hasDeleteRights()) { + $arSubMenu[] = $this->getButton('DELETE_ELEMENT', array( + 'ONCLICK' => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_CONFIRM') . "')) location.href='" . + static::getUrl(array_merge($this->additionalUrlParams, + array( + 'ID' => $this->data[$this->pk()], + 'action' => 'delete', + 'lang' => LANGUAGE_ID, + 'restore_query' => 'Y', + ))) . "'", + 'ICON' => 'delete' + )); + } + + if (count($arSubMenu)) { + $menu[] = array('SEPARATOR' => 'Y'); + $menu[] = $this->getButton('ACTIONS', array( + 'MENU' => $arSubMenu, + 'ICON' => 'btn_new' + )); + } + + return $menu; + } + + /** + * {@inheritdoc} + */ + public function show() + { + if (!$this->hasReadRights()) { + $this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN')); + $this->showMessages(); + + return false; + } + + $context = new \CAdminContextMenu($this->getMenu()); + $context->Show(); + + $this->tabControl->BeginPrologContent(); + $this->showMessages(); + $this->showProlog(); + $this->tabControl->EndPrologContent(); + + $this->tabControl->BeginEpilogContent(); + $this->showEpilog(); + $this->tabControl->EndEpilogContent(); + + $query = $this->additionalUrlParams; + + if (isset($_REQUEST[$this->pk()])) { + $query[$this->pk()] = $_REQUEST[$this->pk()]; + } + elseif (isset($_REQUEST['SECTION_ID']) && $_REQUEST['SECTION_ID']) { + $model = $this->getModel(); + $this->data[$model::getSectionField()] = $_REQUEST['SECTION_ID']; + } + + $this->tabControl->Begin(array( + 'FORM_ACTION' => static::getUrl($query) + )); + + foreach ($this->tabs as $tabSettings) { + if ($tabSettings['VISIBLE']) { + $this->showTabElements($tabSettings); + } + } + + $this->showEditPageButtons(); + $this->tabControl->ShowWarnings('editform', array()); //TODO: дописать + $this->tabControl->Show(); + } + + /** + * Отображение кнопок для управления элементом на странице редактирования. + */ + protected function showEditPageButtons() + { + $listHelper = static::getHelperClass(AdminListHelper::className()); + + $this->tabControl->Buttons(array( + 'back_url' => $listHelper::getUrl(array_merge($this->additionalUrlParams, + array( + 'lang' => LANGUAGE_ID, + 'restore_query' => 'Y', + ))) + )); + } + + /** + * Отрисовка верхней части страницы. + * + * @api + */ + protected function showProlog() + { + } + + /** + * Отрисовка нижней части страницы. По-умолчанию рисует все поля, которые не попали в вывод, как input hidden. + * + * @api + */ + protected function showEpilog() + { + echo bitrix_sessid_post(); + + $interfaceSettings = static::getInterfaceSettings(); + + foreach ($interfaceSettings['FIELDS'] as $code => $settings) { + if (!isset($settings['TAB']) AND isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) { + print ''; + } + } + } + + /** + * Отрисовывает вкладку со всеми привязанными к ней полями. + * + * @param $tabSettings + * + * @internal + */ + private function showTabElements($tabSettings) + { + $this->setContext(AdminEditHelper::OP_SHOW_TAB_ELEMENTS); + $this->tabControl->BeginNextFormTab(); + + foreach ($this->getFields() as $code => $fieldSettings) { + $widget = $this->createWidgetForField($code, $this->data); + $fieldTab = $widget->getSettings('TAB'); + $fieldOnCurrentTab = ($fieldTab == $tabSettings['DIV'] OR $tabSettings['DIV'] == 'DEFAULT_TAB'); + + if (!$fieldOnCurrentTab) { + continue; + } + + $fieldSettings = $widget->getSettings(); + + if (isset($fieldSettings['VISIBLE']) && $fieldSettings['VISIBLE'] === false) { + continue; + } + + $this->tabControl->BeginCustomField($code, $widget->getSettings('TITLE')); + $pkField = ($code == $this->pk()); + $widget->showBasicEditField($pkField); + $this->tabControl->EndCustomField($code); + } + } + + /** + * Обработка запроса редактирования страницы. Этапы: + *
      + *
    • Проверка прав пользователя
    • + *
    • Создание виджетов для каждого поля
    • + *
    • Изменение данных модели каждым виджетом (исходя из его внутренней логики)
    • + *
    • Валидация значений каждого поля соответствующим виджетом
    • + *
    • Проверка на ошибики валидации
    • + *
    • В случае неудачи - выход из функции
    • + *
    • В случае успеха - обновление или добавление элемента в БД
    • + *
    • Постобработка данных модели каждым виджетом
    • + *
    + * + * @return bool + * + * @see HelperWidget::processEditAction(); + * @see HelperWidget::processAfterSaveAction(); + * + * @internal + */ + protected function editAction() + { + $this->setContext(AdminEditHelper::OP_EDIT_ACTION_BEFORE); + + if (!$this->hasWriteRights()) { + $this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_WRITE_FORBIDDEN')); + + return false; + } + + $allWidgets = array(); + + foreach ($this->getFields() as $code => $settings) { + $widget = $this->createWidgetForField($code, $this->data); + $widget->processEditAction(); + $this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors()); + $allWidgets[] = $widget; + } + + $this->addErrors($this->validationErrors); + $success = empty($this->validationErrors); + + if ($success) { + $this->setContext(AdminEditHelper::OP_EDIT_ACTION_AFTER); + $existing = false; + $id = isset($_REQUEST['FIELDS'][$this->pk()]) ? $_REQUEST['FIELDS'][$this->pk()] : $_REQUEST[$this->pk()]; + + if ($id) { + /** @var DataManager $className */ + $className = static::getModel(); + // Если имеется primary key, то модель уже существующая, пытаемся найти ее в БД + $existing = $className::getById($id)->fetch(); + } + + if ($existing) { + $result = $this->saveElement($id); + } + else { + $result = $this->saveElement(); + } + + if ($result) { + if (!$result->isSuccess()) { + $this->addErrors($result->getErrorMessages()); + + return false; + } + } + else { + // TODO Вывод ошибки + return false; + } + + $this->data[$this->pk()] = $result->getId(); + + foreach ($allWidgets as $widget) { + /** @var HelperWidget $widget */ + $widget->setData($this->data); + $widget->processAfterSaveAction(); + } + + if (!$existing) { + LocalRedirect(static::getUrl(array('ID' => $result->getId(), 'lang' => LANGUAGE_ID))); + } + + return true; + } + + return false; + } + + /** + * Функция загрузки элемента из БД. Можно переопределить, если требуется сложная логика и нет возможности + * определить её в модели. + * + * @param array $select + * + * @return bool + * @api + */ + protected function loadElement($select = array()) + { + if (isset($_REQUEST[$this->pk()])) { + $className = static::getModel(); + $result = $className::getById($_REQUEST[$this->pk()]); + + return $result->fetch(); + } + + return false; + } + + /** + * Сохранение элемента. Можно переопределить, если требуется сложная логика и нет возможности определить её + * в модели. + * + * Операциями сохранения модели занимается EntityManager. + * + * @param bool $id + * + * @return \Bitrix\Main\Entity\AddResult|\Bitrix\Main\Entity\UpdateResult + * + * @throws \Exception + * + * @see EntityManager + * + * @api + */ + protected function saveElement($id = null) + { + $className = static::getModel(); + $entityManager = new EntityManager($className, $this->data, $id, $this); + + $saveResult = $entityManager->save(); + $this->addNotes($entityManager->getNotes()); + + return $saveResult; + } + + /** + * Удаление элемента. Можно переопределить, если требуется сложная логика и нет возможности определить её в модели. + * + * @param $id + * + * @return bool|\Bitrix\Main\Entity\DeleteResult + * + * @throws \Exception + * + * @api + */ + protected function deleteElement($id) + { + if (!$this->hasDeleteRights()) { + $this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_FORBIDDEN')); + + return false; + } + + $className = static::getModel(); + $entityManager = new EntityManager($className, array(), $id, $this); + + $deleteResult = $entityManager->delete(); + $this->addNotes($entityManager->getNotes()); + + return $deleteResult; + } + + /** + * Выполнение кастомных операций над объектом в процессе редактирования. + * + * @param string $action Название операции. + * @param int|null $id ID элемента. + * + * @see AdminEditHelper::fillMenu() + * + * @api + */ + protected function customActions($action, $id) + { + if ($action == 'delete' AND !is_null($id)) { + $result = $this->deleteElement($id); + + if(!$result->isSuccess()){ + $this->addErrors($result->getErrorMessages()); + } + + $listHelper = static::getHelperClass(AdminListHelper::className()); + $redirectUrl = $listHelper::getUrl(array_merge( + $this->additionalUrlParams, + array('restore_query' => 'Y') + )); + + LocalRedirect($redirectUrl); + } + } + + /** + * Устанавливает заголовок исходя из данных текущего элемента. + * + * @see $data + * @see AdminBaseHelper::setTitle() + * + * @api + */ + protected function setElementTitle() + { + if (!empty($this->data)) { + $title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_TITLE', array('#ID#' => $this->data[$this->pk()])); + } + else { + $title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_NEW_ELEMENT'); + } + + $this->setTitle($title); + } + + /** + * @return \CAdminForm + */ + public function getTabControl() + { + return $this->tabControl; + } + + /** + * @inheritdoc + */ + public static function getUrl(array $params = array()) + { + return static::getViewURL(static::getViewName(), static::$editPageUrl, $params); + } +} \ No newline at end of file diff --git a/lib/helper/AdminInterface.php b/lib/helper/AdminInterface.php new file mode 100644 index 0000000..c5b3dd2 --- /dev/null +++ b/lib/helper/AdminInterface.php @@ -0,0 +1,210 @@ + + * @author Artem Yarygin + */ +abstract class AdminInterface +{ + /** + * Список зарегистрированных интерфейсов + * @var string + */ + public static $registeredInterfaces = array(); + + /** + * Описание интерфейса админки: списка табов и полей. Метод должен вернуть массив вида: + * + * ``` + * array( + * 'TAB_1' => array( + * 'NAME' => Loc::getMessage('VENDOR_MODULE_ENTITY_TAB_1_NAME'), + * 'FIELDS' => array( + * 'FIELD_1' => array( + * 'WIDGET' => new StringWidget(), + * 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_1_TITLE'), + * ... + * ), + * 'FIELD_2' => array( + * 'WIDGET' => new NumberWidget(), + * 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_2_TITLE'), + * ... + * ), + * ... + * ) + * ), + * 'TAB_2' => array( + * 'NAME' => Loc::getMessage('VENDOR_MODULE_ENTITY_TAB_2_NAME'), + * 'FIELDS' => array( + * 'FIELD_3' => array( + * 'WIDGET' => new DateTimeWidget(), + * 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_3_TITLE'), + * ... + * ), + * 'FIELD_4' => array( + * 'WIDGET' => new UserWidget(), + * 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_4_TITLE'), + * ... + * ), + * ... + * ) + * ), + * ... + * ) + * ``` + * + * Где TAB_1..2 - символьные коды табов, FIELD_1..4 - название столбцов в таблице сущности. TITLE для поля задавать + * не обязательно, в этому случае он будет запрашиваться из модели. + * + * Более подробную информацию о формате описания настроек виджетов см. в классе HelperWidget. + * + * @see DigitalWand\AdminHelper\Widget\HelperWidget + * + * @return array[] + */ + abstract public function fields(); + + /** + * Список классов хелперов с настройками. Метод должен вернуть массив вида: + * + * ``` + * array( + * '\Vendor\Module\Entity\AdminInterface\EntityListHelper' => array( + * 'BUTTONS' => array( + * 'RETURN_TO_LIST' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_RETURN_TO_LIST')), + * 'ADD_ELEMENT' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_ADD_ELEMENT'), + * ... + * ) + * ), + * '\Vendor\Module\Entity\AdminInterface\EntityEditHelper' => array( + * 'BUTTONS' => array( + * 'LIST_CREATE_NEW' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_LIST_CREATE_NEW')), + * 'LIST_CREATE_NEW_SECTION' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_LIST_CREATE_NEW_SECTION'), + * ... + * ) + * ) + * ) + * ``` + * + * или + * + * ``` + * array( + * '\Vendor\Module\Entity\AdminInterface\EntityListHelper', + * '\Vendor\Module\Entity\AdminInterface\EntityEditHelper' + * ) + * ``` + * + * Где: + *
      + *
    • `Vendor\Module\Entity\AdminInterface` - namespace до реализованных классов AdminHelper. + *
    • `BUTTONS` - ключ для массива с описанием элементов управления (подробнее в методе getButton() + * класса AdminBaseHelper). + *
    • `LIST_CREATE_NEW`, `LIST_CREATE_NEW_SECTION`, `RETURN_TO_LIST`, `ADD_ELEMENT` - символьные код элементов + * управления. + *
    • `EntityListHelper` и `EntityEditHelper` - реализованные классы хелперов. + * + * Оба формата могут сочетаться друг с другом. + * + * @see \DigitalWand\AdminHelper\Helper\AdminBaseHelper::getButton() + * + * @return string[] + */ + abstract public function helpers(); + + /** + * Список зависимых админских интерфейсов, которые будут зарегистрированы при регистраци админского интерфейса, + * например, админские интерфейсы разделов. + * + * @return string[] + */ + public function dependencies() + { + return array(); + } + + /** + * Регистрируем поля, табы и кнопки. + */ + public function registerData() + { + $fieldsAndTabs = array('FIELDS' => array(), 'TABS' => array()); + $tabsWithFields = $this->fields(); + + // приводим массив хелперов к формату класс => настройки + $helpers = array(); + + foreach ($this->helpers() as $key => $value) { + if (is_array($value)) { + $helpers[$key] = $value; + } + else { + $helpers[$value] = array(); + } + } + + $helperClasses = array_keys($helpers); + /** + * @var \Bitrix\Main\Entity\DataManager + */ + $model = $helperClasses[0]::getModel(); + foreach ($tabsWithFields as $tabCode => $tab) { + $fieldsAndTabs['TABS'][$tabCode] = $tab['NAME']; + + foreach ($tab['FIELDS'] as $fieldCode => $field) { + if (empty($field['TITLE']) && $model) { + $field['TITLE'] = $model::getEntity()->getField($fieldCode)->getTitle(); + } + + $field['TAB'] = $tabCode; + $fieldsAndTabs['FIELDS'][$fieldCode] = $field; + } + } + + AdminBaseHelper::setInterfaceSettings($fieldsAndTabs, $helpers, $helperClasses[0]::getModule()); + + foreach ($helperClasses as $helperClass) { + /** + * @var AdminBaseHelper $helperClass + */ + $helperClass::setInterfaceClass(get_called_class()); + } + } + + /** + * Регистрация интерфейса и его зависимостей. + */ + public static function register() + { + if (!in_array(get_called_class(), static::$registeredInterfaces)) { + static::$registeredInterfaces[] = get_called_class(); + + $adminInterface = new static(); + $adminInterface->registerData(); + + foreach ($adminInterface->dependencies() as $adminInterfaceClass) { + $adminInterfaceClass::register(); + } + } + } +} \ No newline at end of file diff --git a/lib/helper/AdminListHelper.php b/lib/helper/AdminListHelper.php index c029a05..41d0a59 100644 --- a/lib/helper/AdminListHelper.php +++ b/lib/helper/AdminListHelper.php @@ -1,769 +1,1380 @@ - - *
    • static protected $model
    • - *
    • static public $module
    • - *
    • static protected $editViewName
    • - *
    • static protected $viewName
    • - *
    - * - * Этого будет дастаточно для получения минимальной функциональности - * Также данный класс может использоваться для отображения всплывающих окон с возможностью выбора элемента из списка - * - * @see AdminBaseHelper::$model - * @see AdminBaseHelper::$module - * @see AdminBaseHelper::$editViewName - * @see AdminBaseHelper::$viewName - * @package AdminHelper - */ -abstract class AdminListHelper extends AdminBaseHelper -{ - const OP_GROUP_ACTION = 'AdminListHelper::__construct_groupAction'; - const OP_ADMIN_VARIABLES_FILTER = 'AdminListHelper::prepareAdminVariables_filter'; - const OP_ADMIN_VARIABLES_HEADER = 'AdminListHelper::prepareAdminVariables_header'; - const OP_GET_DATA_BEFORE = 'AdminListHelper::getData_before'; - const OP_ADD_ROW_CELL = 'AdminListHelper::addRowCell'; - const OP_CREATE_FILTER_FORM = 'AdminListHelper::createFilterForm'; - const OP_CHECK_FILTER = 'AdminListHelper::checkFilter'; - const OP_EDIT_ACTION = 'AdminListHelper::editAction'; - - /** - * @var bool - * Является ли список всплывающим окном для выбора элементов из списка. - * В этой версии не должно быть операций удаления/перехода к редактированию. - */ - protected $isPopup = false; - - /** - * @var string - * Название функции, вызываемой при даблклике на строке списка, в случае, если список выводится в режиме - * всплывающего окна - */ - protected $popupClickFunctionName = 'selectRow'; - - /** - * @var string - * Код функции, вызываемой при клике на строке списка - * @see AdminListHelper::genPipupActionJS() - */ - protected $popupClickFunctionCode; - - /** - * @var array - * Массив с заголовками таблицы - * @see \CAdminList::AddHeaders() - */ - protected $arHeader = array(); - - /** - * @var array - * параметры фильтрации списка в классическим битриксовом формате - */ - protected $arFilter = array(); - - /** - * @var array - * Массив, хранящий тип фильтра для данного поля. Позволяет избежать лишнего парсинга строк. - */ - protected $filterTypes = array(); - - /** - * @var array - * Поля, предназначенные для фильтрации - * @see \CAdminList::InitFilter(); - */ - protected $arFilterFields = array(); - - /** - * Список полей, для которых доступна фильтрация - * @var array - * @see \CAdminFilter::__construct(); - */ - protected $arFilterOpts = array(); - - /** - * @var \CAdminList - */ - protected $list; - - /** - * @var string - * Префикс таблицы. Нужен, чтобы обеспечить уникальность относительно других админ. интерфейсов. - * Без его добавления к конструктору таблицы повычается вероятность, что возникнет конфликт с таблицей из другого - * административного интерфейса, в результате чего неправильно будет работать паджинация, фильтрация. Вероятны - * ошибки запросов к БД. - */ - static protected $tablePrefix = "digitalwand_admin_helper_"; - - /** - * @var array - * Массив с настройками контекстного меню. - */ - protected $contextMenu = array(); - - /** - * @var array массив со списком групповых действий над таблицей. - * Ключ - код действия. Знаение - перевод. - * @see \CAdminList::AddGroupActionTable() - */ - protected $groupActionsList = array(); - - /** - * @var array - * @see \CAdminList::AddGroupActionTable() - */ - protected $groupActionsParams = array(); - - /** - * @var array - * @see \CAdminList::AddFooter(); - */ - protected $footer = array(); - - /** - * @var string - * HTML верхней части таблицы - * @api - */ - public $prologHtml; - - /** - * @var string - * HTML нижней части таблицы - * @api - */ - public $epilogHtml; - - - /** - * Производится инициализация переменных, обработка запросов на редактирование - * - * @param array $fields - * @param bool $isPopup - * @throws \Bitrix\Main\ArgumentException - */ - public function __construct($fields, $isPopup = false) - { - $this->isPopup = $isPopup; - parent::__construct($fields); - - $this->restoreLastGetQuery(); - $this->prepareAdminVariables(); - $this->addContextMenu(); - $this->addGroupActions(); - - if (isset($_REQUEST['action'])) { - $id = isset($_REQUEST['ID']) ? $_REQUEST['ID'] : null; - $this->customActions($_REQUEST['action'], $id); - } - - $className = static::getModel(); - $oSort = new \CAdminSorting($this->getListTableID(), static::pk(), "desc"); - $this->list = new \CAdminList($this->getListTableID(), $oSort); - $this->list->InitFilter($this->arFilterFields); - - if ($this->list->EditAction() AND $this->hasRights()) { - global $FIELDS; - foreach ($FIELDS as $id => $fields) { - if (!$this->list->IsUpdated($id)) { - continue; - } - - $id = intval($id); - $this->editAction($id, $fields); - } - } - - if ($IDs = $this->list->GroupAction() AND $this->hasRights()) { - - if ($_REQUEST['action_target'] == 'selected') { - $this->setContext(AdminListHelper::OP_GROUP_ACTION); - $IDs = array(); - - //Текущий фильтр должен быть модифицирован виждтами - //для соответствия результатов фильтрации тому, что видит пользователь в интерфейсе. - $raw = array( - 'SELECT' => $this->pk(), - 'FILTER' => $this->arFilter, - 'SORT' => array() - ); - foreach ($this->fields as $code => $settings) { - $widget = $this->createWidgetForField($code); - $widget->changeGetListOptions($this->arFilter, $raw['SELECT'], $raw['SORT'], $raw); - } - - $res = $className::getList(array( - 'filter' => $this->arFilter, - 'select' => array($this->pk()), - )); - while ($el = $res->Fetch()) { - $IDs[] = $el[$this->pk()]; - } - } - - $filteredIDs = array(); - foreach ($IDs as $id) { - - if (strlen($id) <= 0) { - continue; - } - - $filteredIDs[] = IntVal($id); - } - - $this->groupActions($IDs, $_REQUEST['action']); - } - - if ($this->isPopup()) { - $this->genPopupActionJS(); - } - } - - /** - * Подготавливает переменные, используемые для инициализации списка. - */ - protected function prepareAdminVariables() - { - - $this->arHeader = array(); - $this->arFilter = array(); - $this->arFilterFields = array(); - - $arFilter = array(); - $this->filterTypes = array(); - - $this->arFilterOpts = array(); - - foreach ($this->fields as $code => $settings) { - - $widget = $this->createWidgetForField($code); - $settings = $widget->getSettings(); - - if ((isset($settings['FILTER']) AND $settings['FILTER'] != false) OR !isset($settings['FILTER'])) { - - $this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_FILTER); - - $filterVarName = 'find_' . $code; - $this->arFilterFields[] = $filterVarName; - - $filterType = ''; - if (is_string($settings['FILTER'])) { - $filterType = $settings['FILTER']; - } - - if (isset($_REQUEST[$filterVarName]) - AND !isset($_REQUEST['del_filter']) - AND $_REQUEST['del_filter'] != 'Y' - ) { - $arFilter[$filterType . $code] = $_REQUEST[$filterVarName]; - $this->filterTypes[$code] = $filterType; - } - - $this->arFilterOpts[$code] = $widget->getSettings('TITLE'); - } - - if (!isset($settings['HEADER']) OR $settings['HEADER'] != false) { - $this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_HEADER); - $this->arHeader[] = array( - "id" => $code, - "content" => $widget->getSettings('TITLE'), - "sort" => $code, - "default" => true - ); - } - } - - if ($this->checkFilter($arFilter)) { - $this->arFilter = $arFilter; - } - } - - /** - * Подготавливает массив с настройками футера таблицы Bitrix - * @param \CAdminResult $res - результат выборки данных - */ - protected function addFooter($res) - { - $this->footer = array( - array( - "title" => Loc::getMessage("MAIN_ADMIN_LIST_SELECTED"), - "value" => $res->SelectedRowsCount(), - ), - array( - "counter" => true, - "title" => Loc::getMessage("MAIN_ADMIN_LIST_CHECKED"), - "value" => "0", - ), - ); - } - - /** - * Подготавливает массив с настройками контекстного меню. - * По-умолчанию добавлена кнопка "создать элемент". - * @api - * @see $contextMenu - */ - protected function addContextMenu() - { - $this->contextMenu = array(); - - if (!$this->isPopup() && $this->hasRights()) { - $this->contextMenu[] = array( - 'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_CREATE_NEW'), - 'LINK' => static::getEditPageURL($this->additionalUrlParams), - 'TITLE' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_CREATE_NEW'), - 'ICON' => 'btn_new' - ); - } - } - - /** - * Подготавливает массив с настройками групповых действий над списком - * @see $groupActionsList - * @api - */ - protected function addGroupActions() - { - if (!$this->isPopup()) { - $this->groupActionsList = array('delete' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE")); - } - } - - /** - * Обработчик групповых операций. - * По-умолчанию прописаны операции активации/деактивации и удаления. - * - * @api - * @param array $IDs - * @param string $action - */ - protected function groupActions($IDs, $action) - { - if (!isset($_REQUEST['model'])) { - $className = static::getModel(); - } else { - $className = $_REQUEST['model']; - } - - if ($action == 'delete') { - foreach ($IDs as $id) { - $className::delete($id); - } - } - } - - /** - * Основной цикл отображения списка. Этапы: - *
      - *
    • Вывод заголовков страницы
    • - *
    • Определение списка видимых колонок и колонок, участвующих в выборке.
    • - *
    • Создание виджета для каждого поля выборки
    • - *
    • Модификация параметров запроса каждым из виджетов
    • - *
    • Выборка данных
    • - *
    • Вывод строк таблицы. Во время итерации по строкам возможна модификация данных строки.
    • - *
    • Отрисовка футера таблиы, добавление контекстного меню
    • - *
    - * - * @param array $sort Настройки сортировки. - * - * @see AdminListHelper::getList(); - * @see AdminListHelper::modifyRowData(); - * @see AdminListHelper::addRowCell(); - * @see AdminListHelper::addRow(); - * @see HelperWidget::changeGetListOptions(); - */ - public function getData($sort) - { - $this->setContext(AdminListHelper::OP_GET_DATA_BEFORE); - - $this->list->AddHeaders($this->arHeader); - $visibleColumns = $this->list->GetVisibleHeaderColumns(); - - $className = static::getModel(); - $visibleColumns[] = static::pk(); - - $raw = array( - 'SELECT' => $visibleColumns, - 'FILTER' => $this->arFilter, - 'SORT' => $sort - ); - - - foreach ($this->fields as $name => $settings) { - if ((isset($settings['VIRTUAL']) AND $settings['VIRTUAL'] == true)) { - $key = array_search($name, $visibleColumns); - if($key !== false){ - unset($visibleColumns[$key]); - unset($this->arFilter[$name]); - unset($sort[$name]); - } - } - if (isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) { - $visibleColumns[] = $name; - } - } - $visibleColumns = array_unique($visibleColumns); - - foreach ($this->fields as $code => $settings) { - $widget = $this->createWidgetForField($code); - $widget->changeGetListOptions($this->arFilter, $visibleColumns, $sort, $raw); - } - - $res = $this->getList($className, $this->arFilter, $visibleColumns, $sort, $raw); - - $res = new \CAdminResult($res, $this->getListTableID()); - $res->NavStart(); - - $this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES"))); - - while ($data = $res->NavNext(false)) { - $this->modifyRowData($data); - list($link, $name) = $this->addRow($data); - $row = $this->list->AddRow($data[$this->pk()], $data, $link, $name); - foreach ($this->fields as $code => $settings) { - $this->addRowCell($row, $code, $data); - - } - $actions = $this->addRowActions($data); - $row->AddActions($actions); - } - - $this->addFooter($res); - $this->list->AddFooter($this->footer); - $this->list->AddGroupActionTable($this->groupActionsList, $this->groupActionsParams); - $this->list->AddAdminContextMenu($this->contextMenu); - - $this->list->BeginPrologContent(); - echo $this->prologHtml; - $this->list->EndPrologContent(); - - $this->list->BeginEpilogContent(); - echo $this->epilogHtml; - $this->list->EndEpilogContent(); - - $this->list->CheckListMode(); - } - - /** - * Производит выборку данных. Функцию стоит переопределить в случае, если необходима своя логика, и её нельзя - * вынести в класс модели. - * - * @param DataManager $className - * @param array $filter - * @param array $select - * @param array $sort - * @param array $raw - * @api - * - * @return Result - */ - protected function getList($className, $filter, $select, $sort, $raw) - { - $parameters = array( - 'filter' => $filter, - 'select' => $select, - 'order' => $sort - ); - - /** @var Result $res */ - $res = $className::getList($parameters); - - return $res; - } - - /** - * Является ли список всплывающим окном для выбора элементов из списка. - * В этой версии не должно быть операций удаления/перехода к редактированию. - * @return boolean - */ - public function isPopup() - { - return $this->isPopup; - } - - /** - * Настройки строки таблицы - * @param array $data данные текущей строки БД - * @return array возвращает ссылку на детальную страницу и её название - * @api - */ - protected function addRow($data) - { - if ($this->isPopup()) { - return array(); - - } else { - $query = array_merge($this->additionalUrlParams, array( - 'lang' => LANGUAGE_ID, - static::pk() => $data[static::pk()] - )); - - return array(static::getEditPageURL($query)); - } - } - - /** - * Преобразует данные строки, перед тем как добавлять их в список - * @api - * @param $data - * @see AdminListHelper::getList(); - */ - protected function modifyRowData(&$data) - { - - } - - /** - * Для каждой ячейки таблицы создаёт виджет соответствующего типа. - * Виджет подготавливает необходимый HTML для списка - * - * @param \CAdminListRow $row - * @param $code - сивольный код поля - * @param $data - данные текущей строки - * @see HelperWidget::genListHTML() - */ - protected function addRowCell($row, $code, $data) - { - $widget = $this->createWidgetForField($code, $data); - $this->setContext(AdminListHelper::OP_ADD_ROW_CELL); - $widget->genListHTML($row, $data); - - } - - /** - * Возвращает массив со списком действий при клике правой клавишей мыши на строке таблицы - * По-умолчанию: - *
      - *
    • Редактировать элемент
    • - *
    • Удалить элемент
    • - *
    • Если это всплывающее окно - запустить кастомную JS-функцию.
    • - *
    - * - * @see CAdminListRow::AddActions - * - * @api - * @param $data - данные текущей строки - * @return array - */ - protected function addRowActions($data) - { - $actions = array(); - - if ($this->isPopup()) { - $jsData = \CUtil::PhpToJSObject($data); - $actions['select'] = array( - "ICON" => "select", - "DEFAULT" => true, - "TEXT" => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_SELECT"), - "ACTION" => 'javascript:' . $this->popupClickFunctionName . '(' . $jsData . ')' - ); - - } else { - $viewQueryString = 'module=' . static::getModule() . '&view=' . static::$viewName; - $query = array_merge($this->additionalUrlParams, - array($this->pk() => $data[$this->pk()])); - if ($this->hasRights()) { - $actions['edit'] = array( - "ICON" => "edit", - "DEFAULT" => true, - "TEXT" => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_EDIT"), - "ACTION" => $this->list->ActionRedirect(static::getEditPageURL($query)) - ); - - $actions['delete'] = array( - "ICON" => "delete", - "TEXT" => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE"), - "ACTION" => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_CONFIRM') . "')) " . $this->list->ActionDoGroup($data[$this->pk()], - "delete", $viewQueryString) - ); - } - } - - return $actions; - } - - /** - * Функция определяет js-функцию для двойонго клика по строке. - * Вызывается в том случае, елси окно открыто в режиме попапа. - * По-умолчанию выводится скрипт-заглушка. - * @api - */ - protected function genPopupActionJS() - { - //Тестовый пример. Необходимо переопределить! - $this->popupClickFunctionCode = ''; - } - - /** - * Выводит форму фильтрации списка - */ - public function createFilterForm() - { - $this->setContext(AdminListHelper::OP_CREATE_FILTER_FORM); - print '
    '; - - $oFilter = new \CAdminFilter($this->getListTableID() . '_filter', $this->arFilterOpts); - $oFilter->Begin(); - foreach ($this->arFilterOpts as $code => $name) { - $widget = $this->createWidgetForField($code); - $widget->genFilterHTML(); - } - - $oFilter->Buttons(array( - "table_id" => $this->getListTableID(), - "url" => static::getListPageURL($this->additionalUrlParams), - "form" => "find_form", - )); - $oFilter->End(); - - print '
    '; - } - - /** - * Производит проверку корректности данных (в массиве $_REQUEST), переданных в фильтр - * @TODO: нужно сделать вывод сообщений об ошибке фильтрации. - * @param $arFilter - * @return bool - */ - protected function checkFilter($arFilter) - { - $this->setContext(AdminListHelper::OP_CHECK_FILTER); - $filterValidationErrors = array(); - foreach ($this->filterTypes as $code => $type) { - $widget = $this->createWidgetForField($code); - $value = $arFilter[$type . $code]; - if (!$widget->checkFilter($type, $value)) { - $filterValidationErrors = array_merge($filterValidationErrors, - $widget->getValidationErrors()); - } - } - - return empty($filterValidationErrors); - } - - /** - * Возвращает ID таблицы, который не должен конфликтовать с ID в других разделах админки, а также нормально - * парситься в JS - * - * @return string - */ - protected function getListTableID() - { - return str_replace('.', '', static::$tablePrefix . $this->table()); - } - - /** - * Сохранение полей для отной записи, отредактированной в списке. - * Этапы: - *
      - *
    • Выборка элемента по ID, чтобы удостовериться, что он существует. В противном случае возвращается - * ошибка
    • - *
    • Создание виджета для каждой ячейки, валидация значений поля
    • - *
    • TODO: вывод ошибок валидации
    • - *
    • Сохранение записи
    • - *
    • Вывод ошибок сохранения, если таковые появились
    • - *
    • Модификация данных сроки виджетами.
    • - *
    - * - * @param int $id ID записи в БД - * @param array $fields Поля с изменениями - * - * @see HelperWidget::processEditAction(); - * @see HelperWidget::processAfterSaveAction(); - */ - protected function editAction($id, $fields) - { - $this->setContext(AdminListHelper::OP_EDIT_ACTION); - - $className = static::getModel(); - $el = $className::getById($id); - if ($el->getSelectedRowsCount() == 0) { - $this->list->AddGroupError(Loc::getMessage("DIGITALWAND_ADMIN_HELPER_SAVE_ERROR"), $id); - return; - } - - $allWidgets = array(); - foreach ($fields as $key => $value) { - $widget = $this->createWidgetForField($key, $fields); - - $widget->processEditAction(); - $this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors()); - $allWidgets[] = $widget; - } - //FIXME: может, надо добавить вывод ошибок ДО сохранения?.. - $this->addErrors($this->validationErrors); - - $result = $className::update($id, $fields); - $errors = $result->getErrorMessages(); - if (empty($this->validationErrors) AND !empty($errors)) { - $fieldList = implode("\n", $errors); - $this->list->AddGroupError(Loc::getMessage("DIGITALWAND_ADMIN_HELPER_SAVE_ERROR") . " " . $fieldList, $id); - } - - if (!empty($errors)) { - foreach ($allWidgets as $widget) { - /** @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget */ - $widget->setData($fields); - $widget->processAfterSaveAction(); - } - } - } - - /** - * Выводит сформированный список. - * Сохраняет обработанный GET-запрос в сессию - */ - public function show() - { - $this->showMessages(); - $this->list->DisplayList(); - - if ($this->isPopup()) { - print $this->popupClickFunctionCode; - } - - $this->saveGetQuery(); - } - - /** - * Сохраняет параметры запроса для поторного использования после возврата с других страниц (к примеру, после - * перехода с детальной обратно в список - чтобы вернуться в точности в тот раздел, с которого ранее ушли) - */ - private function saveGetQuery() - { - $_SESSION['LAST_GET_QUERY'][get_called_class()] = $_GET; - } - - /** - * Восстанавливает последний GET-запрос, если в текущем задан параметр restore_query=Y - */ - private function restoreLastGetQuery() - { - if (!isset($_SESSION['LAST_GET_QUERY'][get_called_class()]) - OR !isset($_REQUEST['restore_query']) - OR $_REQUEST['restore_query'] != 'Y' - ) { - return; - } - - $_GET = array_merge($_GET, $_SESSION['LAST_GET_QUERY'][get_called_class()]); - $_REQUEST = array_merge($_REQUEST, $_SESSION['LAST_GET_QUERY'][get_called_class()]); - } + + *
  • static protected $model
  • + * + * + * Этого будет дастаточно для получения минимальной функциональности + * Также данный класс может использоваться для отображения всплывающих окон с возможностью выбора элемента из списка + * + * @see AdminBaseHelper::$model + * @see AdminBaseHelper::$module + * @see AdminBaseHelper::$editViewName + * @see AdminBaseHelper::$viewName + * @package AdminHelper + * + * @author Nik Samokhvalov + * @author Artem Yarygin + */ +abstract class AdminListHelper extends AdminBaseHelper +{ + const OP_GROUP_ACTION = 'AdminListHelper::__construct_groupAction'; + const OP_ADMIN_VARIABLES_FILTER = 'AdminListHelper::prepareAdminVariables_filter'; + const OP_ADMIN_VARIABLES_HEADER = 'AdminListHelper::prepareAdminVariables_header'; + const OP_GET_DATA_BEFORE = 'AdminListHelper::getData_before'; + const OP_ADD_ROW_CELL = 'AdminListHelper::addRowCell'; + const OP_CREATE_FILTER_FORM = 'AdminListHelper::createFilterForm'; + const OP_CHECK_FILTER = 'AdminListHelper::checkFilter'; + const OP_EDIT_ACTION = 'AdminListHelper::editAction'; + + /** + * @var bool + * Является ли список всплывающим окном для выбора элементов из списка. + * В этой версии не должно быть операций удаления/перехода к редактированию. + */ + protected $isPopup = false; + /** + * @var string + * Название поля, в котором хранится результат выбора во всплывающем окне + */ + protected $fieldPopupResultName = ''; + /** + * @var string + * Уникальный индекс поля, в котором хранится результат выбора во всплывающем окне + */ + protected $fieldPopupResultIndex = ''; + protected $sectionFields = array(); + /** + * @var string + * Название столбца, в котором хранится название элемента + */ + protected $fieldPopupResultElTitle = ''; + /** + * @var string + * Название функции, вызываемой при даблклике на строке списка, в случае, если список выводится в режиме + * всплывающего окна + */ + protected $popupClickFunctionName = 'selectRow'; + /** + * @var string + * Код функции, вызываемой при клике на строке списка + * @see AdminListHelper::genPipupActionJS() + */ + protected $popupClickFunctionCode; + /** + * @var array + * Массив с заголовками таблицы + * @see \CAdminList::AddHeaders() + */ + protected $arHeader = array(); + /** + * @var array + * параметры фильтрации списка в классическим битриксовом формате + */ + protected $arFilter = array(); + /** + * @var array + * Массив, хранящий тип фильтра для данного поля. Позволяет избежать лишнего парсинга строк. + */ + protected $filterTypes = array(); + /** + * @var array + * Поля, предназначенные для фильтрации + * @see \CAdminList::InitFilter(); + */ + protected $arFilterFields = array(); + /** + * Список полей, для которых доступна фильтрация + * @var array + * @see \CAdminFilter::__construct(); + */ + protected $arFilterOpts = array(); + /** + * @var \CAdminList + */ + protected $list; + /** + * @var string + * Префикс таблицы. Нужен, чтобы обеспечить уникальность относительно других админ. интерфейсов. + * Без его добавления к конструктору таблицы повычается вероятность, что возникнет конфликт с таблицей из другого + * административного интерфейса, в результате чего неправильно будет работать паджинация, фильтрация. Вероятны + * ошибки запросов к БД. + */ + static protected $tablePrefix = "digitalwand_admin_helper_"; + /** + * @var array + * @see \CAdminList::AddGroupActionTable() + */ + protected $groupActionsParams = array(); + /** + * Текущие параметры пагинации, + * требуются для составления смешанного списка разделов и элементов + * @var array + */ + protected $navParams = array(); + /** + * Количество элементов смешанном списке + * @see AdminListHelper::CustomNavStart + * @var int + */ + protected $totalRowsCount = 0; + /** + * Массив для слияния столбцов элементов и разделов + * @var array + */ + protected $tableColumnsMap = array(); + /** + * @var string + * HTML верхней части таблицы + * @api + */ + public $prologHtml; + + /** + * @var string + * HTML нижней части таблицы + * @api + */ + public $epilogHtml; + + /** + * Производится инициализация переменных, обработка запросов на редактирование + * + * @param array $fields + * @param bool $isPopup + * @throws \Bitrix\Main\ArgumentException + */ + public function __construct(array $fields, $isPopup = false) + { + $this->isPopup = $isPopup; + + if ($this->isPopup) { + $this->fieldPopupResultName = preg_replace("/[^a-zA-Z0-9_:\\[\\]]/", "", $_REQUEST['n']); + $this->fieldPopupResultIndex = preg_replace("/[^a-zA-Z0-9_:]/", "", $_REQUEST['k']); + $this->fieldPopupResultElTitle = $_REQUEST['eltitle']; + } + + parent::__construct($fields); + + $this->restoreLastGetQuery(); + $this->prepareAdminVariables(); + + $className = static::getModel(); + $oSort = new \CAdminSorting($this->getListTableID(), static::pk(), "desc"); + $this->list = new \CAdminList($this->getListTableID(), $oSort); + $this->list->InitFilter($this->arFilterFields); + + if ($this->list->EditAction() AND $this->hasWriteRights()) { + global $FIELDS; + foreach ($FIELDS as $id => $fields) { + if (!$this->list->IsUpdated($id)) { + continue; + } + $this->editAction($id, $fields); + } + } + if ($IDs = $this->list->GroupAction() AND $this->hasWriteRights()) { + if ($_REQUEST['action_target'] == 'selected') { + $this->setContext(AdminListHelper::OP_GROUP_ACTION); + $IDs = array(); + + //Текущий фильтр должен быть модифицирован виждтами + //для соответствия результатов фильтрации тому, что видит пользователь в интерфейсе. + $raw = array( + 'SELECT' => $this->pk(), + 'FILTER' => $this->arFilter, + 'SORT' => array() + ); + + foreach ($this->fields as $code => $settings) { + $widget = $this->createWidgetForField($code); + $widget->changeGetListOptions($this->arFilter, $raw['SELECT'], $raw['SORT'], $raw); + } + + $res = $className::getList(array( + 'filter' => $this->arFilter, + 'select' => array($this->pk()), + )); + + while ($el = $res->Fetch()) { + $IDs[] = $el[$this->pk()]; + } + } + + $filteredIDs = array(); + + foreach ($IDs as $id) { + if (strlen($id) <= 0) { + continue; + } + $filteredIDs[] = IntVal($id); + } + $this->groupActions($IDs, $_REQUEST['action']); + }elseif (isset($_REQUEST['action']) || isset($_REQUEST['action_button']) && count($this->getErrors()) == 0 ) { + $listHelperClass = $this->getHelperClass(AdminListHelper::className()); + $className = $listHelperClass::getModel(); + $id = isset($_GET['ID']) ? $_GET['ID'] : null; + $action = isset($_REQUEST['action']) ? $_REQUEST['action'] : $_REQUEST['action_button']; + if($action!='edit' && $_REQUEST['cancel'] != 'Y'){ + $params = $_GET; + unset($params['action']); + unset($params['action_button']); + $this->customActions($action, $id); + $sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className()); + + if ($sectionEditHelperClass) { + $element = $className::getById($id)->Fetch(); + if ($element[$className::getSectionField()]) { + $params['ID'] = $element[$className::getSectionField()]; + } + } + + LocalRedirect($listHelperClass::getUrl($params)); + } + } + + if ($this->isPopup()) { + $this->genPopupActionJS(); + } + + // Получаем параметры навигации + $navUniqSettings = array('sNavID' => $this->getListTableID()); + $this->navParams = array( + 'nPageSize' => \CAdminResult::GetNavSize($navUniqSettings), + 'navParams' => \CAdminResult::GetNavParams($navUniqSettings) + ); + } + + /** + * Подготавливает переменные, используемые для инициализации списка. + */ + protected function prepareAdminVariables() + { + $this->arHeader = array(); + $this->arFilter = array(); + $this->arFilterFields = array(); + $arFilter = array(); + $this->filterTypes = array(); + $this->arFilterOpts = array(); + + foreach ($this->fields as $code => $settings) { + $widget = $this->createWidgetForField($code); + + if ((isset($settings['FILTER']) AND $settings['FILTER'] != false) OR !isset($settings['FILTER'])) { + $this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_FILTER); + $filterVarName = 'find_' . $code; + $this->arFilterFields[] = $filterVarName; + $filterType = ''; + + if (is_string($settings['FILTER'])) { + $filterType = $settings['FILTER']; + } + + if (isset($_REQUEST[$filterVarName]) + AND !isset($_REQUEST['del_filter']) + AND $_REQUEST['del_filter'] != 'Y' + ) { + $arFilter[$filterType . $code] = $_REQUEST[$filterVarName]; + $this->filterTypes[$code] = $filterType; + } + + $this->arFilterOpts[$code] = $widget->getSettings('TITLE'); + } + + if (!isset($settings['HEADER']) OR $settings['HEADER'] != false) { + $this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_HEADER); + $mergedColumn = false; + // проверяем есть ли столбец раздела с таким названием + if ($widget->getSettings('LIST_TITLE')) { + $sectionHeader = $this->getSectionsHeader(); + foreach ($sectionHeader as $sectionColumn) { + if ($sectionColumn['content'] == $widget->getSettings('LIST_TITLE')) { + // добавляем столбец элементов в карту столбцов + $this->tableColumnsMap[$code] = $sectionColumn['id']; + $mergedColumn = true; + break; + } + } + } + if (!$mergedColumn) { + $this->arHeader[] = array( + "id" => $code, + "content" => $widget->getSettings('LIST_TITLE') ? $widget->getSettings('LIST_TITLE') : $widget->getSettings('TITLE'), + "sort" => $code, + "default" => true, + 'admin_list_helper_sort' => $widget->getSettings('LIST_COLUMN_SORT') ? $widget->getSettings('LIST_COLUMN_SORT') : 100 + ); + } + } + } + + if ($this->checkFilter($arFilter)) { + $this->arFilter = $arFilter; + } + + if (static::getHelperClass(AdminSectionEditHelper::className())) { + $model = $this->getModel(); + $this->arFilter[$model::getSectionField()] = $_GET['ID']; + } + } + + /** + * Возвращает список столбцов для разделов + * @return array + */ + public function getSectionsHeader() + { + $arSectionsHeaders = array(); + $sectionHelper = static::getHelperClass(AdminSectionEditHelper::className()); + $sectionsInterfaceSettings = static::getInterfaceSettings($sectionHelper::getViewName()); + $this->sectionFields = $sectionsInterfaceSettings['FIELDS']; + + foreach ($sectionsInterfaceSettings['FIELDS'] as $code => $settings) { + if (isset($settings['HEADER']) && $settings['HEADER'] == true) { + $arSectionsHeaders[] = array( + "id" => $code, + "content" => isset($settings['LIST_TITLE']) ? $settings['LIST_TITLE'] : $settings['TITLE'], + "sort" => $code, + "default" => true, + 'admin_list_helper_sort' => isset($settings['LIST_COLUMN_SORT']) ? $settings['LIST_COLUMN_SORT'] : 100 + ); + } + unset($settings['WIDGET']); + + foreach ($settings as $c => $v) { + $sectionsInterfaceSettings['FIELDS'][$code]['WIDGET']->setSetting($c, $v); + } + } + + return $arSectionsHeaders; + } + + /** + * Производит проверку корректности данных (в массиве $_REQUEST), переданных в фильтр + * @TODO: нужно сделать вывод сообщений об ошибке фильтрации. + * @param $arFilter + * @return bool + */ + protected function checkFilter($arFilter) + { + $this->setContext(AdminListHelper::OP_CHECK_FILTER); + $filterValidationErrors = array(); + foreach ($this->filterTypes as $code => $type) { + $widget = $this->createWidgetForField($code); + $value = $arFilter[$type . $code]; + if (!$widget->checkFilter($type, $value)) { + $filterValidationErrors = array_merge($filterValidationErrors, + $widget->getValidationErrors()); + } + } + + return empty($filterValidationErrors); + } + + /** + * Подготавливает массив с настройками контекстного меню. По-умолчанию добавлена кнопка "создать элемент". + * + * @see $contextMenu + * + * @api + */ + protected function getContextMenu() + { + $contextMenu = array(); + $sectionEditHelper = static::getHelperClass(AdminSectionEditHelper::className()); + if ($sectionEditHelper) { + $this->additionalUrlParams['SECTION_ID'] = $_GET['ID']; + } + + /** + * Если задан для разделов добавляем кнопку создать раздел и + * кнопку на уровень вверх если это не корневой раздел + */ + if ($sectionEditHelper && isset($_GET['ID'])) { + if ($_GET['ID']) { + $params = $this->additionalUrlParams; + $sectionModel = $sectionEditHelper::getModel(); + $section = $sectionModel::getById($_GET['ID'])->Fetch(); + if ($this->isPopup()) { + $params = array_merge($_GET); + } + if ($section[$sectionModel::getSectionField()]) { + $params['ID'] = $section[$sectionModel::getSectionField()]; + } + else { + unset($params['ID']); + } + unset($params['SECTION_ID']); + $contextMenu[] = $this->getButton('LIST_SECTION_UP', array( + 'LINK' => static::getUrl($params), + 'ICON' => 'btn_list' + )); + } + } + + /** + * Добавляем кнопку создать элемент и создать раздел + */ + if (!$this->isPopup() && $this->hasWriteRights()) { + $editHelperClass = static::getHelperClass(AdminEditHelper::className()); + if ($editHelperClass) { + $contextMenu[] = $this->getButton('LIST_CREATE_NEW', array( + 'LINK' => $editHelperClass::getUrl($this->additionalUrlParams), + 'ICON' => 'btn_new' + )); + } + $sectionsHelperClass = static::getHelperClass(AdminSectionEditHelper::className()); + if ($sectionsHelperClass) { + $contextMenu[] = $this->getButton('LIST_CREATE_NEW_SECTION', array( + 'LINK' => $sectionsHelperClass::getUrl($this->additionalUrlParams), + 'ICON' => 'btn_new' + )); + } + } + + return $contextMenu; + } + + /** + * Возвращает массив с настройками групповых действий над списком. + * + * @return array + * + * @api + */ + protected function getGroupActions() + { + $result = array(); + + if (!$this->isPopup()) { + if ($this->hasDeleteRights()) { + $result = array('delete' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE")); + } + } + + return $result; + } + + /** + * Обработчик групповых операций. По-умолчанию прописаны операции активации / деактивации и удаления. + * + * @param array $IDs + * @param string $action + * + * @api + */ + protected function groupActions($IDs, $action) + { + if (!isset($_REQUEST['model'])) { + $className = static::getModel(); + } + else { + $className = $_REQUEST['model']; + } + + $sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className()); + $listHelperClass = $this->getHelperClass(AdminListHelper::className()); + + if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) { + $sectionClassName = $sectionEditHelperClass::getModel(); + } + else { + $sectionClassName = $_REQUEST['model-section']; + } + + if ($action == 'delete') { + if ($this->hasDeleteRights()) { + $params = $_GET; + unset($params['action']); + unset($params['action_button']); + unset($params['ID']); + if ($sectionEditHelperClass) { + $element = $className::getById($IDs[0])->Fetch(); + if ($element[$className::getSectionField()]) { + $params['ID'] = $element[$className::getSectionField()]; + } + } + + foreach ($IDs as $id) { + $entityManager = new EntityManager($className, array(), $id, $this); + $result = $entityManager->delete(); + $this->addNotes($entityManager->getNotes()); + if(!$result->isSuccess()){ + + $this->addErrors($result->getErrorMessages()); + break; + } + } + LocalRedirect($listHelperClass::getUrl($params)); + } + else { + $this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN')); + } + } + + if ($action == 'delete-section') { + if ($this->hasDeleteRights()) { + $section = $sectionClassName::getById($IDs[0])->Fetch(); + $params = $_GET; + unset($params['action']); + unset($params['action_button']); + unset($params['ID']); + if ($section[$sectionClassName::getSectionField()]) { + $params['ID'] = $section[$sectionClassName::getSectionField()]; + } + foreach ($IDs as $id) { + $sectionClassName::delete($id); + } + LocalRedirect($listHelperClass::getUrl($params)); + } + else { + $this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN')); + } + } + } + + /** + * Сохранение полей для отной записи, отредактированной в списке. + * Этапы: + *
      + *
    • Выборка элемента по ID, чтобы удостовериться, что он существует. В противном случае возвращается + * ошибка
    • + *
    • Создание виджета для каждой ячейки, валидация значений поля
    • + *
    • TODO: вывод ошибок валидации
    • + *
    • Сохранение записи
    • + *
    • Вывод ошибок сохранения, если таковые появились
    • + *
    • Модификация данных сроки виджетами.
    • + *
    + * + * @param int $id ID записи в БД + * @param array $fields Поля с изменениями + * + * @see HelperWidget::processEditAction(); + * @see HelperWidget::processAfterSaveAction(); + */ + protected function editAction($id, $fields) + { + $this->setContext(AdminListHelper::OP_EDIT_ACTION); + if(strpos($id, 's')===0){ // для раделов другой класс модели + $editHelperClass = $this->getHelperClass(AdminSectionEditHelper::className()); + $sectionsInterfaceSettings = static::getInterfaceSettings($editHelperClass::getViewName()); + $className = $editHelperClass::getModel(); + $id = str_replace('s','',$id); + }else{ + $className = static::getModel(); + $sectionsInterfaceSettings = false; + } + $el = $className::getById($id); + if ($el->getSelectedRowsCount() == 0) { + $this->list->AddGroupError(Loc::getMessage("MAIN_ADMIN_SAVE_ERROR"), $id); + return; + } + + // замена кодов для столбцов элементов соединенных со столбцами разделов + if($sectionsInterfaceSettings==false){ + $tableColumnsMap = array_flip($this->tableColumnsMap); + $replacedFields = array(); + foreach($fields as $key => $value){ + if(!empty($tableColumnsMap[$key])) { + $key = $tableColumnsMap[$key]; + } + $replacedFields[$key] = $value; + } + $fields = $replacedFields; + } + + $allWidgets = array(); + foreach ($fields as $key => $value) { + if($sectionsInterfaceSettings!==false){ // для разделов свои виджеты + $widget = $sectionsInterfaceSettings['FIELDS'][$key]['WIDGET']; + }else{ + $widget = $this->createWidgetForField($key, $fields); // для элементов свои + } + + $widget->processEditAction(); + $this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors()); + $allWidgets[] = $widget; + } + //FIXME: может, надо добавить вывод ошибок ДО сохранения?.. + $this->addErrors($this->validationErrors); + + $result = $className::update($id, $fields); + $errors = $result->getErrorMessages(); + if (empty($this->validationErrors) AND !empty($errors)) { + $fieldList = implode("\n", $errors); + $this->list->AddGroupError(Loc::getMessage("MAIN_ADMIN_SAVE_ERROR") . " " . $fieldList, $id); + } + + if (!empty($errors)) { + foreach ($allWidgets as $widget) { + /** @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget */ + $widget->setData($fields); + $widget->processAfterSaveAction(); + } + } + } + + /** + * Является ли список всплывающим окном для выбора элементов из списка. + * В этой версии не должно быть операций удаления/перехода к редактированию. + * + * @return boolean + */ + public function isPopup() + { + return $this->isPopup; + } + + /** + * Функция определяет js-функцию для двойонго клика по строке. + * Вызывается в том случае, если окно открыто в режиме попапа. + * + * @api + */ + protected function genPopupActionJS() + { + $this->popupClickFunctionCode = ''; + } + + /** + * Основной цикл отображения списка. Этапы: + *
      + *
    • Вывод заголовков страницы
    • + *
    • Определение списка видимых колонок и колонок, участвующих в выборке.
    • + *
    • Создание виджета для каждого поля выборки
    • + *
    • Модификация параметров запроса каждым из виджетов
    • + *
    • Выборка данных
    • + *
    • Вывод строк таблицы. Во время итерации по строкам возможна модификация данных строки.
    • + *
    • Отрисовка футера таблицы, добавление контекстного меню
    • + *
    + * + * @param array $sort Настройки сортировки. + * + * @see AdminListHelper::getList(); + * @see AdminListHelper::getMixedData(); + * @see AdminListHelper::modifyRowData(); + * @see AdminListHelper::addRowCell(); + * @see AdminListHelper::addRow(); + * @see HelperWidget::changeGetListOptions(); + */ + public function buildList($sort) + { + $this->setContext(AdminListHelper::OP_GET_DATA_BEFORE); + + $headers = $this->arHeader; + + $sectionEditHelper = static::getHelperClass(AdminSectionEditHelper::className()); + + if ($sectionEditHelper) { // если есть реализация класса AdminSectionEditHelper, значит используются разделы + $sectionHeaders = $this->getSectionsHeader(); + foreach ($sectionHeaders as $sectionHeader) { + foreach ($headers as $i => $elementHeader) { + if ($sectionHeader['id'] == $elementHeader['id']) { + unset($headers[$i]); + } + } + } + $headers = array_merge($headers, $sectionHeaders); + } + + // сортировка столбцов с сохранением исходной позиции в + // массиве для развнозначных элементов + // массив $headers модифицируется + $this->mergeSortHeader($headers); + + $this->list->AddHeaders($headers); + $visibleColumns = $this->list->GetVisibleHeaderColumns(); + + if ($sectionEditHelper) { + $modelClass = $this->getModel(); + $elementFields = array_keys($modelClass::getEntity()->getFields()); + $sectionsVisibleColumns = array(); + foreach ($visibleColumns as $k => $v) { + if (isset($this->sectionFields[$v])) { + if(!in_array($k, $elementFields)){ + unset($visibleColumns[$k]); + } + $sectionsVisibleColumns[] = $v; + } + } + $visibleColumns = array_values($visibleColumns); + $visibleColumns = array_merge($visibleColumns, array_keys($this->tableColumnsMap)); + } + + $className = static::getModel(); + $visibleColumns[] = static::pk(); + $sectionsVisibleColumns[] = static::sectionPk(); + + $raw = array( + 'SELECT' => $visibleColumns, + 'FILTER' => $this->arFilter, + 'SORT' => $sort + ); + + foreach ($this->fields as $name => $settings) { + if ((isset($settings['VIRTUAL']) AND $settings['VIRTUAL'] == true)) { + $key = array_search($name, $visibleColumns); + unset($visibleColumns[$key]); + unset($this->arFilter[$name]); + unset($sort[$name]); + } + if (isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) { + $visibleColumns[] = $name; + } + } + + $visibleColumns = array_unique($visibleColumns); + $sectionsVisibleColumns = array_unique($sectionsVisibleColumns); + + // Поля для селекта (перевернутый массив) + $listSelect = array_flip($visibleColumns); + foreach ($this->fields as $code => $settings) { + $widget = $this->createWidgetForField($code); + $widget->changeGetListOptions($this->arFilter, $visibleColumns, $sort, $raw); + // Множественные поля не должны быть в селекте + if (!empty($settings['MULTIPLE'])) { + unset($listSelect[$code]); + } + } + // Поля для селекта (множественные поля отфильтрованы) + $listSelect = array_flip($listSelect); + + if ($sectionEditHelper) // Вывод разделов и элементов в одном списке + { + $mixedData = $this->getMixedData($sectionsVisibleColumns, $visibleColumns, $sort, $raw); + $res = new \CDbResult; + $res->InitFromArray($mixedData); + $res = new \CAdminResult($res, $this->getListTableID()); + $res->nSelectedCount = $this->totalRowsCount; + // используем кастомный NavStart что бы определить правильное количество страниц и элементов в списке + $this->customNavStart($res); + $this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES"))); + while ($data = $res->NavNext(false)) { + $this->modifyRowData($data); + if ($data['IS_SECTION']) // для разделов своя обработка + { + list($link, $name) = $this->getRow($data, $this->getHelperClass(AdminSectionEditHelper::className())); + $row = $this->list->AddRow('s' . $data[$this->pk()], $data, $link, $name); + foreach ($this->sectionFields as $code => $settings) { + if (in_array($code, $sectionsVisibleColumns)) { + $this->addRowSectionCell($row, $code, $data); + } + } + $row->AddActions($this->getRowActions($data, true)); + } + else // для элементов своя + { + $this->modifyRowData($data); + list($link, $name) = $this->getRow($data); + // объединение полей элемента с полями раздела + foreach ($this->tableColumnsMap as $elementCode => $sectionCode) { + if (isset($data[$elementCode])) { + $data[$sectionCode] = $data[$elementCode]; + } + } + $row = $this->list->AddRow($data[$this->pk()], $data, $link, $name); + foreach ($this->fields as $code => $settings) { + $this->addRowCell($row, $code, $data, + isset($this->tableColumnsMap[$code]) ? $this->tableColumnsMap[$code] : false); + } + $row->AddActions($this->getRowActions($data)); + } + } + } + else // Обычный вывод элементов без использования разделов + { + $res = $this->getData($className, $this->arFilter, $listSelect, $sort, $raw); + $res = new \CAdminResult($res, $this->getListTableID()); + $res->NavStart(); + $this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES"))); + while ($data = $res->NavNext(false)) { + $this->modifyRowData($data); + list($link, $name) = $this->getRow($data); + $row = $this->list->AddRow($data[$this->pk()], $data, $link, $name); + foreach ($this->fields as $code => $settings) { + $this->addRowCell($row, $code, $data); + } + $row->AddActions($this->getRowActions($data)); + } + } + + $this->list->AddFooter($this->getFooter($res)); + $this->list->AddGroupActionTable($this->getGroupActions(), $this->groupActionsParams); + $this->list->AddAdminContextMenu($this->getContextMenu()); + + $this->list->BeginPrologContent(); + echo $this->prologHtml; + $this->list->EndPrologContent(); + + $this->list->BeginEpilogContent(); + echo $this->epilogHtml; + $this->list->EndEpilogContent(); + + // добавляем ошибки в CAdminList для режимов list и frame + if(in_array($_GET['mode'], array('list','frame')) && is_array($this->getErrors())) { + foreach($this->getErrors() as $error) { + $this->list->addGroupError($error); + } + } + + $this->list->CheckListMode(); + } + + /** + * Функция сортировки столбцов c сохранением порядка равнозначных элементов + * @param $array + */ + protected function mergeSortHeader(&$array) + { + // для сортировки нужно хотя бы 2 элемента + if (count($array) < 2) return; + + // делим массив пополам + $halfway = count($array) / 2; + $array1 = array_slice($array, 0, $halfway); + $array2 = array_slice($array, $halfway); + + // реукрсивно сортируем каждую половину + $this->mergeSortHeader($array1); + $this->mergeSortHeader($array2); + + // если последний элемент первой половины меньше или равен первому элементу + // второй половины, то просто соединяем массивы + if ($this->mergeSortHeaderCompare(end($array1), $array2[0]) < 1) { + $array = array_merge($array1, $array2); + return; + } + + // соединяем 2 отсортированных половины в один отсортированный массив + $array = array(); + $ptr1 = $ptr2 = 0; + while ($ptr1 < count($array1) && $ptr2 < count($array2)) { + // собираем в 1 массив последовательную цепочку + // элементов из 2-х отсортированных половинок + if ($this->mergeSortHeaderCompare($array1[$ptr1], $array2[$ptr2]) < 1) { + $array[] = $array1[$ptr1++]; + } + else { + $array[] = $array2[$ptr2++]; + } + } + + // если в исходных массивах что-то осталось забираем в основной массив + while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++]; + while ($ptr2 < count($array2)) $array[] = $array2[$ptr2++]; + + return; + } + + /** + * Функция сравнения столбцов по их весу в сортировке + * @param $a + * @param $b + * @return int + */ + public function mergeSortHeaderCompare($a, $b) + { + $a = $a['admin_list_helper_sort']; + $b = $b['admin_list_helper_sort']; + if ($a == $b) { + return 0; + } + + return ($a < $b) ? -1 : 1; + } + + /** + * Получение смешанного списка из разделов и элементов. + * + * @param $sectionsVisibleColumns + * @param $elementVisibleColumns + * @param $sort + * @param $raw + * @return array + */ + protected function getMixedData($sectionsVisibleColumns, $elementVisibleColumns, $sort, $raw) + { + $sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className()); + $returnData = array(); + /** + * @var DataManager $sectionModel + */ + $sectionModel = $sectionEditHelperClass::getModel(); + $sectionFilter = array($sectionModel::getSectionField() => $_GET['ID']); + + $raw['SELECT'] = array_unique($raw['SELECT']); + + // при использовании в качестве popup окна исключаем раздел из выборке + // что бы не было возможности сделать раздел родителем самого себя + if (!empty($_REQUEST['self_id'])) { + $sectionFilter['!' . $this->sectionPk()] = $_REQUEST['self_id']; + } + + $sectionSort = array(); + $limitData = $this->getLimits(); + // добавляем к общему количеству элементов количество разделов + $this->totalRowsCount = $sectionModel::getCount($sectionFilter); + foreach ($sort as $field => $direction) { + if (in_array($field, $sectionsVisibleColumns)) { + $sectionSort[$field] = $direction; + } + } + // добавляем к выборке разделы + $rsSections = $sectionModel::getList(array( + 'filter' => $sectionFilter, + 'select' => $sectionsVisibleColumns, + 'order' => $sectionSort, + 'limit' => $limitData[1], + 'offset' => $limitData[0], + )); + + while ($section = $rsSections->fetch()) { + $section['IS_SECTION'] = true; + $returnData[] = $section; + } + + // расчитываем offset и limit для элементов + if (count($returnData) > 0) { + $elementOffset = 0; + } + else { + $elementOffset = $limitData[0] - $this->totalRowsCount; + } + + // для списка разделов элементы не нужны + if (static::getHelperClass(AdminSectionListHelper::className()) == static::className()) { + return $returnData; + } + + $elementLimit = $limitData[1] - count($returnData); + $elementModel = static::$model; + $elementFilter = $this->arFilter; + $elementFilter[$elementModel::getSectionField()] = $_GET['ID']; + // добавляем к общему количеству элементов количество элементов + $this->totalRowsCount += $elementModel::getCount($elementFilter); + + // возвращае данные без элементов если разделы занимают всю страницу выборки + if (!empty($returnData) && $limitData[0] == 0 && $limitData[1] == $this->totalRowsCount) { + return $returnData; + } + + $elementSort = array(); + foreach ($sort as $field => $direction) { + if (in_array($field, $elementVisibleColumns)) { + $elementSort[$field] = $direction; + } + } + + $elementParams = array( + 'filter' => $elementFilter, + 'select' => $elementVisibleColumns, + 'order' => $elementSort, + ); + if ($elementLimit > 0 && $elementOffset >= 0) { + $elementParams['limit'] = $elementLimit; + $elementParams['offset'] = $elementOffset; + // добавляем к выборке элементы + $rsSections = $elementModel::getList($elementParams); + + while ($element = $rsSections->fetch()) { + $element['IS_SECTION'] = false; + $returnData[] = $element; + } + } + + return $returnData; + } + + /** + * Огранчения выборки из CAdminResult + * @return array + */ + protected function getLimits() + { + if ($this->navParams['navParams']['SHOW_ALL']) { + return array(); + } + else { + if (!intval($this->navParams['navParams']['PAGEN']) OR !isset($this->navParams['navParams']['PAGEN'])) { + $this->navParams['navParams']['PAGEN'] = 1; + } + $from = $this->navParams['nPageSize'] * ((int)$this->navParams['navParams']['PAGEN'] - 1); + + return array($from, $this->navParams['nPageSize']); + } + } + + /** + * Выполняет CDBResult::NavNext с той разницей, что общее количество элементов берется не из count($arResult), + * а из нашего параметра, полученного из SQL-запроса. + * array_slice также не делается. + * + * @param \CAdminResult $res + */ + protected function customNavStart(&$res) + { + $res->NavStart($this->navParams['nPageSize'], + $this->navParams['navParams']['SHOW_ALL'], + (int)$this->navParams['navParams']['PAGEN'] + ); + + $res->NavRecordCount = $this->totalRowsCount; + if ($res->NavRecordCount < 1) + return; + + if ($res->NavShowAll) + $res->NavPageSize = $res->NavRecordCount; + + $res->NavPageCount = floor($res->NavRecordCount / $res->NavPageSize); + if ($res->NavRecordCount % $res->NavPageSize > 0) + $res->NavPageCount++; + + $res->NavPageNomer = + ($res->PAGEN < 1 || $res->PAGEN > $res->NavPageCount + ? + (\CPageOption::GetOptionString("main", "nav_page_in_session", "Y") != "Y" + || $_SESSION[$res->SESS_PAGEN] < 1 + || $_SESSION[$res->SESS_PAGEN] > $res->NavPageCount + ? + 1 + : + $_SESSION[$res->SESS_PAGEN] + ) + : + $res->PAGEN + ); + } + + /** + * Преобразует данные строки, перед тем как добавлять их в список. + * + * @param $data + * + * @see AdminListHelper::getList() + * + * @api + */ + protected function modifyRowData(&$data) + { + } + + /** + * Настройки строки таблицы. + * + * @param array $data Данные текущей строки БД. + * @param bool|string $class Класс хелпера через метод getUrl которого идет получение ссылки. + * + * @return array Возвращает ссылку на детальную страницу и её название. + * + * @api + */ + protected function getRow($data, $class = false) + { + if (empty($class)) { + $class = static::getHelperClass(AdminEditHelper::className()); + } + if ($this->isPopup()) { + return array(); + } + else { + $query = array_merge($this->additionalUrlParams, array( + 'lang' => LANGUAGE_ID, + static::pk() => $data[static::pk()] + )); + + return array($class::getUrl($query)); + } + } + + /** + * Для каждой ячейки(раздела) таблицы создаёт виджет соответствующего типа. + * Виджет подготавливает необходимый HTML для списка. + * + * @param \CAdminListRow $row + * @param $code Сивольный код поля. + * @param $data Данные текущей строки. + * + * @throws Exception + * + * @see HelperWidget::generateRow() + */ + protected function addRowSectionCell($row, $code, $data) + { + $sectionEditHelper = $this->getHelperClass(AdminSectionEditHelper::className()); + if (!isset($this->sectionFields[$code]['WIDGET'])) { + $error = str_replace('#CODE#', $code, 'Can\'t create widget for the code "#CODE#"'); + throw new Exception($error, Exception::CODE_NO_WIDGET); + } + + /** + * @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget + */ + $widget = $this->sectionFields[$code]['WIDGET']; + + $widget->setHelper($this); + $widget->setCode($code); + $widget->setData($data); + $widget->setEntityName($sectionEditHelper::getModel()); + + $this->setContext(AdminListHelper::OP_ADD_ROW_CELL); + $widget->generateRow($row, $data); + } + + /** + * Возвращает массив со списком действий при клике правой клавишей мыши на строке таблицы + * По-умолчанию: + *
      + *
    • Редактировать элемент
    • + *
    • Удалить элемент
    • + *
    • Если это всплывающее окно - запустить кастомную JS-функцию.
    • + *
    + * + * @param $data Данные текущей строки. + * @param $section Признак списка для раздела. + * + * @return array + * + * @see CAdminListRow::AddActions + * + * @api + */ + protected function getRowActions($data, $section = false) + { + $actions = array(); + + if ($this->isPopup()) { + $jsData = \CUtil::PhpToJSObject($data); + $actions['select'] = array( + 'ICON' => 'select', + 'DEFAULT' => true, + 'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SELECT'), + "ACTION" => 'javascript:' . $this->popupClickFunctionName . '(' . $jsData . ')' + ); + } + else { + $viewQueryString = 'module=' . static::getModule() . '&view=' . static::getViewName() . '&entity=' . static::getEntityCode(); + $query = array_merge($this->additionalUrlParams, + array($this->pk() => $data[$this->pk()])); + if ($this->hasWriteRights()) { + $sectionHelperClass = static::getHelperClass(AdminSectionEditHelper::className()); + $editHelperClass = static::getHelperClass(AdminEditHelper::className()); + + $actions['edit'] = array( + 'ICON' => 'edit', + 'DEFAULT' => true, + 'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_EDIT'), + 'ACTION' => $this->list->ActionRedirect($section ? $sectionHelperClass::getUrl($query) : $editHelperClass::getUrl($query)) + ); + } + if ($this->hasDeleteRights()) { + $actions['delete'] = array( + 'ICON' => 'delete', + 'TEXT' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE"), + 'ACTION' => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_CONFIRM') . "')) " . $this->list->ActionDoGroup($data[$this->pk()], + $section ? "delete-section" : "delete", $viewQueryString) + ); + } + } + + return $actions; + } + + /** + * Для каждой ячейки таблицы создаёт виджет соответствующего типа. Виджет подготавливает необходимый HTML-код + * для списка. + * + * @param \CAdminListRow $row Объект строки списка записей. + * @param string $code Сивольный код поля. + * @param array $data Данные текущей строки. + * @param bool $virtualCode + * + * @throws Exception + * + * @see HelperWidget::generateRow() + */ + protected function addRowCell($row, $code, $data, $virtualCode = false) + { + $widget = $this->createWidgetForField($code, $data); + $this->setContext(AdminListHelper::OP_ADD_ROW_CELL); + + // устанавливаем виртуальный код ячейки, используется при слиянии столбцов + if ($virtualCode) { + $widget->setCode($virtualCode); + } + + $widget->generateRow($row, $data); + + if ($virtualCode) { + $widget->setCode($code); + } + } + + /** + * Производит выборку данных. Функцию стоит переопределить в случае, если необходима своя логика, и её нельзя + * вынести в класс модели. + * + * @param DataManager $className + * @param array $filter + * @param array $select + * @param array $sort + * @param array $raw + * + * @return Result + * + * @api + */ + protected function getData($className, $filter, $select, $sort, $raw) + { + $parameters = array( + 'filter' => $filter, + 'select' => $select, + 'order' => $sort + ); + + /** @var Result $res */ + $res = $className::getList($parameters); + + return $res; + } + + /** + * Подготавливает массив с настройками футера таблицы Bitrix + * @param \CAdminResult $res - результат выборки данных + * @see \CAdminList::AddFooter() + * @return array[] + */ + protected function getFooter($res) + { + return array( + $this->getButton('MAIN_ADMIN_LIST_SELECTED', array("value" => $res->SelectedRowsCount())), + $this->getButton('MAIN_ADMIN_LIST_CHECKED', array("value" => $res->SelectedRowsCount()), array( + "counter" => true, + "value" => "0", + )), + ); + } + + /** + * Выводит форму фильтрации списка + */ + public function createFilterForm() + { + $this->setContext(AdminListHelper::OP_CREATE_FILTER_FORM); + print '
    '; + + $oFilter = new \CAdminFilter($this->getListTableID() . '_filter', $this->arFilterOpts); + $oFilter->Begin(); + + foreach ($this->arFilterOpts as $code => $name) { + $widget = $this->createWidgetForField($code); + $widget->showFilterHtml(); + } + + $oFilter->Buttons(array( + "table_id" => $this->getListTableID(), + "url" => static::getUrl($this->additionalUrlParams), + "form" => "find_form", + )); + $oFilter->End(); + + print '
    '; + } + + /** + * Возвращает ID таблицы, который не должен конфликтовать с ID в других разделах админки, а также нормально + * парситься в JS + * + * @return string + */ + protected function getListTableID() + { + return str_replace('.', '', static::$tablePrefix . $this->table()); + } + + /** + * Выводит сформированный список. + * Сохраняет обработанный GET-запрос в сессию + */ + public function show() + { + if (!$this->hasReadRights()) { + $this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN')); + $this->showMessages(); + + return false; + } + $this->showMessages(); + $this->list->DisplayList(); + + if ($this->isPopup()) { + print $this->popupClickFunctionCode; + } + + $this->saveGetQuery(); + } + + /** + * Сохраняет параметры запроса для поторного использования после возврата с других страниц (к примеру, после + * перехода с детальной обратно в список - чтобы вернуться в точности в тот раздел, с которого ранее ушли) + */ + private function saveGetQuery() + { + $_SESSION['LAST_GET_QUERY'][get_called_class()] = $_GET; + } + + /** + * Восстанавливает последний GET-запрос, если в текущем задан параметр restore_query=Y + */ + private function restoreLastGetQuery() + { + if (!isset($_SESSION['LAST_GET_QUERY'][get_called_class()]) + OR !isset($_REQUEST['restore_query']) + OR $_REQUEST['restore_query'] != 'Y' + ) { + return; + } + + $_GET = array_merge($_GET, $_SESSION['LAST_GET_QUERY'][get_called_class()]); + $_REQUEST = array_merge($_REQUEST, $_SESSION['LAST_GET_QUERY'][get_called_class()]); + } + + /** + * @inheritdoc + */ + public static function getUrl(array $params = array()) + { + return static::getViewURL(static::getViewName(), static::$listPageUrl, $params); + } } \ No newline at end of file diff --git a/lib/helper/AdminSectionEditHelper.php b/lib/helper/AdminSectionEditHelper.php new file mode 100644 index 0000000..4119c12 --- /dev/null +++ b/lib/helper/AdminSectionEditHelper.php @@ -0,0 +1,22 @@ + + * @author Artem Yarygin + */ +class AdminSectionEditHelper extends AdminEditHelper +{ +} \ No newline at end of file diff --git a/lib/helper/AdminSectionListHelper.php b/lib/helper/AdminSectionListHelper.php new file mode 100644 index 0000000..783113b --- /dev/null +++ b/lib/helper/AdminSectionListHelper.php @@ -0,0 +1,17 @@ + + * @author Artem Yarygin + */ +class AdminSectionListHelper extends AdminListHelper +{ +} \ No newline at end of file diff --git a/lib/helper/Exception.php b/lib/helper/Exception.php index 41b0801..6feb913 100644 --- a/lib/helper/Exception.php +++ b/lib/helper/Exception.php @@ -4,7 +4,7 @@ class Exception extends \Exception { - const CODE_NO_WIDGET = 1; - const CODE_NO_HL_ENTITY_INFORMATION = 2; + const CODE_NO_WIDGET = 1; + const CODE_NO_HL_ENTITY_INFORMATION = 2; } \ No newline at end of file diff --git a/lib/widget/CheckboxWidget.php b/lib/widget/CheckboxWidget.php index d262b78..f8ca73b 100644 --- a/lib/widget/CheckboxWidget.php +++ b/lib/widget/CheckboxWidget.php @@ -1,4 +1,5 @@ true + ); + + /** + * @inheritdoc + */ + protected function getEditHtml() { - $checked = $this->getValue() == 'Y' ? 'checked' : ''; + $html = ''; + + $modeType = $this->getCheckboxType(); + + switch ($modeType) { + case static::TYPE_STRING: { + $checked = $this->getValue() == self::TYPE_STRING_YES ? 'checked' : ''; + + $html = ''; + $html .= ''; + break; + } + case static::TYPE_INT: + case static::TYPE_BOOLEAN: { + $checked = $this->getValue() == self::TYPE_INT_YES ? 'checked' : ''; + + $html = ''; + $html .= ''; + break; + } + } - return ''; + return $html; } /** - * Генерирует HTML для поля в списке - * - * @see AdminListHelper::addRowCell(); - * - * @param \CAdminListRow $row - * @param array $data - данные текущей строки - * - * @return mixed + * @inheritdoc */ - public function genListHTML(&$row, $data) + public function generateRow(&$row, $data) { + $modeType = $this->getCheckboxType(); + + $globalYes = ''; + $globalNo = ''; + + switch ($modeType) { + case self::TYPE_STRING: { + $globalYes = self::TYPE_STRING_YES; + $globalNo = self::TYPE_STRING_NO; + break; + } + case self::TYPE_INT: + case self::TYPE_BOOLEAN: { + $globalYes = self::TYPE_INT_YES; + $globalNo = self::TYPE_INT_NO; + break; + } + } + if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { - $checked = intval($this->getValue() == 'Y') ? 'checked' : ''; - $js = 'var input = document.getElementsByName(\''.$this->getEditableListInputName().'\')[0]; - input.value = this.checked ? \'Y\' : \'N\';'; - $editHtml - = ' + $checked = intval($this->getValue() == $globalYes) ? 'checked' : ''; + $js = 'var input = document.getElementsByName(\'' . $this->getEditableListInputName() . '\')[0]; + input.value = this.checked ? \'' . $globalYes . '\' : \'' . $globalNo . '\';'; + $editHtml = ' '; + value="' . static::prepareToTagAttr($this->getValue()) . '" + name="' . $this->getEditableListInputName() . '" />'; $row->AddEditField($this->getCode(), $editHtml); } - $value = $this->getValueReadonly(); - $row->AddViewField($this->getCode(), $value); - - } + if (intval($this->getValue() == $globalYes)) { + $value = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES'); + } else { + $value = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO'); + } - protected function getValueReadonly() - { - return intval($this->getValue() == 'Y') ? Loc::getMessage('CHECKBOX_YES') : Loc::getMessage('CHECKBOX_NO'); + $row->AddViewField($this->getCode(), $value); } /** - * Генерирует HTML для поля фильтрации - * - * @see AdminListHelper::createFilterForm(); - * @return mixed + * @inheritdoc */ - public function genFilterHTML() + public function showFilterHtml() { - print ''; - print ''.$this->getSettings('TITLE').''; - print ' '; + $filterHtml .= ''; + + $modeType = $this->getCheckboxType(); + + $langYes = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES'); + $langNo = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO'); + + switch ($modeType) { + case self::TYPE_STRING: { + $filterHtml .= ''; + $filterHtml .= ''; + break; + } + case self::TYPE_INT: + case self::TYPE_BOOLEAN: { + $filterHtml .= ''; + $filterHtml .= ''; + break; + } + } - print ''; - print ''; + $filterHtml .= ''; + $filterHtml .= ''; - print ''; - print ''; + print $filterHtml; } - public function getValue() + /** + * @inheritdoc + */ + public function getValueReadonly() { - $rawValue = parent::getValue(); - if (!is_string($rawValue)) { - return $this->toString($rawValue); + $code = $this->getCode(); + $value = isset($this->data[$code]) ? $this->data[$code] : null; + $modeType = $this->getCheckboxType(); + + switch ($modeType) { + case static::TYPE_STRING: { + $value = $value == 'Y' ? Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES') : Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO'); + break; + } + case static::TYPE_INT: + case static::TYPE_BOOLEAN: { + $value = $value ? Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES') : Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO'); + break; + } } - return $rawValue; + return static::prepareToOutput($value); } - public static function toInt($stringValue) + /** + * @inheritdoc + */ + public function processEditAction() { - return $stringValue == 'Y' ? 1 : 0; - } + parent::processEditAction(); - public static function toString($boolValue) - { - return $boolValue ? 'Y' : 'N'; + if ($this->getCheckboxType() === static::TYPE_BOOLEAN) { + $this->data[$this->getCode()] = (bool) $this->data[$this->getCode()]; + } } - public function processEditAction() + /** + * Получить тип чекбокса по типу поля. + * + * @return mixed + */ + public function getCheckboxType() { - parent::processEditAction(); - if(!isset($this->data[$this->getCode()])){ - $this->data[$this->getCode()] = 'N'; + $fieldType = ''; + $entity = $this->getEntityName(); + $entityMap = $entity::getMap(); + $columnName = $this->getCode(); + + if (!isset($entityMap[$columnName])) { + foreach ($entityMap as $field) { + if ($field->getColumnName() === $columnName) { + $fieldType = $field->getDataType(); + break; + } + } + } else { + $fieldType = $entityMap[$columnName]['data_type']; } + + return $fieldType; } } \ No newline at end of file diff --git a/lib/widget/ComboBoxWidget.php b/lib/widget/ComboBoxWidget.php index 9676882..8ca829b 100644 --- a/lib/widget/ComboBoxWidget.php +++ b/lib/widget/ComboBoxWidget.php @@ -2,12 +2,23 @@ namespace DigitalWand\AdminHelper\Widget; +use Bitrix\Main\Localization\Loc; + +Loc::loadMessages(__FILE__); + /** - * Class ComboBoxWidget Выпадающий список + * Выпадающий список. + * * Доступные опции: *
      *
    • STYLE - inline-стили
    • - *
    • VARIANTS - массив с вариантами занчений или функция для их получения
    • + *
    • VARIANTS - массив с вариантами значений или функция для их получения в формате ключ=>заголовок + * Например: + * [ + * 1=>'Первый пункт', + * 2=>'Второй пункт' + * ] + *
    • *
    • DEFAULT_VARIANT - ID варианта по-умолчанию
    • *
    */ @@ -18,41 +29,114 @@ class ComboBoxWidget extends HelperWidget ); /** - * Генерирует HTML для редактирования поля + * @inheritdoc + * * @see AdminEditHelper::showField(); + * * @param bool $forFilter + * * @return mixed */ - protected function genEditHTML($forFilter = false) + protected function getEditHtml($forFilter = false) { $style = $this->getSettings('STYLE'); + $multiple = $this->getSettings('MULTIPLE'); + $multipleSelected = array(); + + if ($multiple) { + $multipleSelected = $this->getMultipleValue(); + } - $name = $forFilter ? $this->getFilterInputName() : $this->getEditInputName(); - $result = ""; + + if (!$multiple) { + $variantEmpty = array( + '' => array( + 'ID' => '', + 'TITLE' => Loc::getMessage('COMBO_BOX_LIST_EMPTY') + ) + ); + $variants = $variantEmpty + $variants; + } + + $default = $this->getValue(); + + if (is_null($default)) { + $default = $this->getSettings('DEFAULT_VARIANT'); + } + + foreach ($variants as $id => $data) { + $name = strlen($data["TITLE"]) > 0 ? $data["TITLE"] : ""; + $selected = false; + + if ($multiple) { + if (in_array($id, $multipleSelected)) { + $selected = true; + } + } else { + if ($id == $default) { + $selected = true; + } + } + + $result .= ""; + } + + $result .= ""; } - foreach ($variants as $id => $name) { - $result .= ""; + return $result; + } + + /** + * @inheritdoc + */ + public function processEditAction() + { + if ($this->getSettings('MULTIPLE')) { + $sphere = $this->data[$this->getCode()]; + unset($this->data[$this->getCode()]); + + foreach ($sphere as $sphereKey) { + $this->data[$this->getCode()][] = array('VALUE' => $sphereKey); + } } - $result .= ""; + parent::processEditAction(); + } - return $result; + /** + * @inheritdoc + */ + protected function getMultipleEditHtml() + { + return $this->getEditHtml(); } + /** + * @inheritdoc + */ protected function getValueReadonly() { $variants = $this->getVariants(); - $value = $variants[$this->getValue()]; - return $value; + $value = $variants[$this->getValue()]['TITLE']; + + return static::prepareToOutput($value); } /** - * Возвращает массив в формате + * Возвращает массив в следующем формате: * * array( * '123' => array('ID' => 123, 'TITLE' => 'ololo'), @@ -60,52 +144,92 @@ protected function getValueReadonly() * '789' => array('ID' => 789, 'TITLE' => 'pish-pish'), * ) * - * Результат будет выводиться в комбобоксе + * + * Результат будет выводиться в комбобоксе. * @return array */ protected function getVariants() { $variants = $this->getSettings('VARIANTS'); - if (is_callable($variants)) { - $var = call_user_func($variants); + if (is_array($variants) AND !empty($variants)) { + return $this->formatVariants($variants); + } else if (is_callable($variants)) { + $var = $variants(); if (is_array($var)) { - return $var; + return $this->formatVariants($var); } - } else if (is_array($variants) AND !empty($variants)) { - return $variants; } return array(); } /** - * Генерирует HTML для поля в списке - * @see AdminListHelper::addRowCell(); - * @param \CAdminListRow $row - * @param array $data - данные текущей строки - * @return mixed + * Приводит варианты к нужному формату, если они заданы в виде одномерного массива. + * + * @param $variants + * + * @return array */ - public function genListHTML(&$row, $data) + protected function formatVariants($variants) { - if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { - $variants = $this->getVariants(); - $row->AddSelectField($this->getCode(), $variants, array('style' => 'width:90%')); + $formatted = array(); + + foreach ($variants as $id => $data) { + if (!is_array($data)) { + $formatted[$id] = array( + 'ID' => $id, + 'TITLE' => $data + ); + } + } + + return $formatted; + } + /** + * @inheritdoc + */ + public function generateRow(&$row, $data) + { + if ($this->settings['EDIT_IN_LIST'] AND !$this->settings['READONLY']) { + $row->AddInputField($this->getCode(), array('style' => 'width:90%')); } else { $row->AddViewField($this->getCode(), $this->getValueReadonly()); } } /** - * Генерирует HTML для поля фильтрации - * @see AdminListHelper::createFilterForm(); - * @return mixed + * @inheritdoc */ - public function genFilterHTML() + public function showFilterHtml() { print ''; print '' . $this->getSettings('TITLE') . ''; - print '' . $this->genEditHTML(true) . ''; + print '' . $this->getEditHtml(true) . ''; print ''; } + + /** + * @inheritdoc + */ + protected function getMultipleValueReadonly() + { + $variants = $this->getVariants(); + $values = $this->getMultipleValue(); + $result = ''; + + if (empty($variants)) { + $result = 'Не удалось получить данные для выбора'; + } else { + foreach ($variants as $id => $data) { + $name = strlen($data["TITLE"]) > 0 ? $data["TITLE"] : ""; + + if (in_array($id, $values)) { + $result .= static::prepareToOutput($name) . '
    '; + } + } + } + + return $result; + } } \ No newline at end of file diff --git a/lib/widget/DateTimeWidget.php b/lib/widget/DateTimeWidget.php index b3d824c..437474a 100644 --- a/lib/widget/DateTimeWidget.php +++ b/lib/widget/DateTimeWidget.php @@ -2,74 +2,76 @@ namespace DigitalWand\AdminHelper\Widget; -use Bitrix\Main\Type\Date; -use Bitrix\Main\Type\DateTime; - class DateTimeWidget extends HelperWidget { - /** - * Генерирует HTML для редактирования поля - * @see AdminEditHelper::showField(); - * @return mixed - */ - protected function genEditHTML() - { - ob_start(); - global $APPLICATION; - $APPLICATION->IncludeComponent("bitrix:main.calendar", "", - array( - "SHOW_INPUT" => "Y", - "FORM_NAME" => "", - "INPUT_NAME" => $this->getEditInputName(), - "INPUT_VALUE" => $this->getValue(), - "INPUT_VALUE_FINISH" => "", - "SHOW_TIME" => "Y", - "HIDE_TIMEBAR" => "N" - ) - ); - $html = ob_get_contents(); - ob_end_clean(); - - return $html; - } - - /** - * Генерирует HTML для поля в списке - * @see AdminListHelper::addRowCell(); - * @param \CAdminListRow $row - * @param array $data - данные текущей строки - * @return mixed - */ - public function genListHTML(&$row, $data) - { - if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { - $row->AddCalendarField($this->getCode()); - } else { - $row->AddViewField($this->getCode(), $this->getValue()); - } - } + /** + * Генерирует HTML для редактирования поля + * @see AdminEditHelper::showField(); + * @return mixed + */ + protected function getEditHtml() + { + return \CAdminCalendar::CalendarDate($this->getEditInputName(), ConvertTimeStamp(strtotime($this->getValue()), "FULL"), 10, true); + } - /** - * Генерирует HTML для поля фильтрации - * @see AdminListHelper::createFilterForm(); - * @return mixed - */ - public function genFilterHTML() - { - list($inputNameFrom, $inputNameTo) = $this->getFilterInputName(); + /** + * Генерирует HTML для поля в списке + * @see AdminListHelper::addRowCell(); + * @param CAdminListRow $row + * @param array $data - данные текущей строки + * @return mixed + */ + public function generateRow(&$row, $data) + { + if (isset($this->settings['EDIT_IN_LIST']) AND $this->settings['EDIT_IN_LIST']) + { + $row->AddCalendarField($this->getCode()); + } + else + { + $arDate = ParseDateTime($this->getValue()); - print ''; - print '' . $this->settings['TITLE'] . ''; - print '' . CalendarPeriod($inputNameFrom, $$inputNameFrom, $inputNameTo, $$inputNameTo, "find_form") . ''; - } + if ($arDate['YYYY'] < 10) + { + $stDate = '-'; + } + else + { + $stDate = ConvertDateTime($this->getValue(), "DD.MM.YYYY HH:MI:SS", "ru"); + } - protected function setValue($value) - { - if (is_string($value)) { - $value = new DateTime($value); - } - return parent::setValue($value); - } + $row->AddViewField($this->getCode(), $stDate); + } + } + /** + * Генерирует HTML для поля фильтрации + * @see AdminListHelper::createFilterForm(); + * @return mixed + */ + public function showFilterHtml() + { + list($inputNameFrom, $inputNameTo) = $this->getFilterInputName(); + print ''; + print '' . $this->settings['TITLE'] . ''; + print '' . CalendarPeriod($inputNameFrom, $$inputNameFrom, $inputNameTo, $$inputNameTo, "find_form") . ''; + } -} + /** + * Сконвертируем дату в формат Mysql + * @return boolean + */ + public function processEditAction() + { + try + { + $this->setValue(new \Bitrix\Main\Type\Datetime($this->getValue())); + } catch (\Exception $e) + { + } + if (!$this->checkRequired()) + { + $this->addError('REQUIRED_FIELD_ERROR'); + } + } +} \ No newline at end of file diff --git a/lib/widget/FileWidget.php b/lib/widget/FileWidget.php index e59aaff..003ea2e 100644 --- a/lib/widget/FileWidget.php +++ b/lib/widget/FileWidget.php @@ -1,128 +1,386 @@ + *
  • DESCRIPTION_FIELD - bool нужно ли поле описания
  • + *
  • MULTIPLE - bool является ли поле множественным
  • + *
  • IMAGE - bool отображать ли изображение файла, для старого вида отображения
  • + * + */ class FileWidget extends HelperWidget { - static protected $defaults = array( + protected static $defaults = array( + 'IMAGE' => false, + 'DESCRIPTION_FIELD' => false, 'EDIT_IN_LIST' => false, - 'FILTER' => false + 'FILTER' => false, + 'UPLOAD' => true, + 'MEDIALIB' => true, + 'FILE_DIALOG' => true, + 'CLOUD' => true, + 'DELETE' => true, + 'EDIT' => true, ); /** - * Генерирует HTML для редактирования поля - * @return mixed + * {@inheritdoc} + */ + public function __construct(array $settings = array()) + { + Loc::loadMessages(__FILE__); + + parent::__construct($settings); + } + + /** + * {@inheritdoc} */ - protected function genEditHTML() + protected function getEditHtml() { - return \CFileInput::Show($this->getEditInputName('_FILE'), ($this->getValue() > 0 ? $this->getValue() : 0), - array( - "IMAGE" => "N", - "PATH" => "Y", - "FILE_SIZE" => "Y", - "ALLOW_UPLOAD" => "I", - ), array( - 'upload' => true, - 'medialib' => false, - 'file_dialog' => false, - 'cloud' => false, - 'del' => true, - 'description' => false, - ) - ); + if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) { + $html = FileInput::createInstance(array( + 'name' => $this->getEditInputName('_FILE'), + 'description' => $this->getSettings('DESCRIPTION_FIELD'), + 'upload' => $this->getSettings('UPLOAD'), + 'allowUpload' => 'I', + 'medialib' => $this->getSettings('MEDIALIB'), + 'fileDialog' => $this->getSettings('FILE_DIALOG'), + 'cloud' => $this->getSettings('CLOUD'), + 'delete' => $this->getSettings('DELETE'), + 'edit' => $this->getSettings('EDIT'), + 'maxCount' => 1 + ))->show($this->getValue()); + } else { + $html = \CFileInput::Show($this->getEditInputName('_FILE'), ($this->getValue() > 0 ? $this->getValue() : 0), + array( + 'IMAGE' => $this->getSettings('IMAGE') === true ? 'Y' : 'N', + 'PATH' => 'Y', + 'FILE_SIZE' => 'Y', + 'ALLOW_UPLOAD' => 'I', + ), array( + 'upload' => $this->getSettings('UPLOAD'), + 'medialib' => $this->getSettings('MEDIALIB'), + 'file_dialog' => $this->getSettings('FILE_DIALOG'), + 'cloud' => $this->getSettings('CLOUD'), + 'del' => $this->getSettings('DELETE'), + 'description' => $this->getSettings('DESCRIPTION_FIELD'), + ) + ); + } + + if ($this->getValue()) { + $html .= ''; + } + + return $html; } /** - * Генерирует HTML для поля в списке - * @see AdminListHelper::addRowCell(); - * @param \CAdminListRow $row - * @param array $data - данные текущей строки - * @return mixed + * {@inheritdoc} */ - public function genListHTML(&$row, $data) + protected function getMultipleEditHtml() { - $file = \CFile::GetPath($data[$this->code]); - $res = \CFile::GetByID($data[$this->code]); - $fileInfo = $res->Fetch(); + $inputHidden = array(); + $inputName = array(); + + if (!empty($this->data['ID'])) { + $entityName = $this->entityName; + + $rsEntityData = $entityName::getList(array( + 'select' => array('REFERENCE_' => $this->getCode() . '.*'), + 'filter' => array('=ID' => $this->data['ID']) + )); + + while ($referenceData = $rsEntityData->fetch()) { + $inputName[$this->code . '[' . $referenceData['REFERENCE_ID'] . ']'] = $referenceData['REFERENCE_VALUE']; + $inputHidden[$referenceData['REFERENCE_ID']] = $referenceData['REFERENCE_VALUE']; + } + } - if (!$file) - { - $html = ""; + if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) { + $html = \Bitrix\Main\UI\FileInput::createInstance(array( + 'name' => $this->code . '[n#IND#]', + 'description' => $this->getSettings('DESCRIPTION_FIELD'), + 'upload' => $this->getSettings('UPLOAD'), + 'allowUpload' => 'I', + 'medialib' => $this->getSettings('MEDIALIB'), + 'fileDialog' => $this->getSettings('FILE_DIALOG'), + 'cloud' => $this->getSettings('CLOUD'), + 'delete' => $this->getSettings('DELETE'), + 'edit' => $this->getSettings('EDIT') + ))->show($inputName); + } else { + $html = \CFileInput::ShowMultiple($inputName, $this->code . '[n#IND#]', + array( + 'IMAGE' => $this->getSettings('IMAGE') === true ? 'Y' : 'N', + 'PATH' => 'Y', + 'FILE_SIZE' => 'Y', + 'DIMENSIONS' => 'Y', + 'IMAGE_POPUP' => 'Y', + ), + false, + array( + 'upload' => $this->getSettings('UPLOAD'), + 'medialib' => $this->getSettings('MEDIALIB'), + 'file_dialog' => $this->getSettings('FILE_DIALOG'), + 'cloud' => $this->getSettings('CLOUD'), + 'del' => $this->getSettings('DELETE'), + 'description' => $this->getSettings('DESCRIPTION_FIELD'), + ) + ); } - else - { - $html = ''.$fileInfo['FILE_NAME'].' ('.$fileInfo['FILE_DESCRIPTION'].')'.''; + + foreach ($inputHidden as $key => $input) { + if (!empty($input)) { + $html .= ' + '; + } } - $row->AddViewField($this->code,$html); + return $html; } + + /** + * {@inheritdoc} + */ + public function generateRow(&$row, $data) + { + $html = ''; + + if ($this->getSettings('MULTIPLE')) { + + } else { + $path = \CFile::GetPath($data[$this->code]); + $rsFile = \CFile::GetByID($data[$this->code]); + $file = $rsFile->Fetch(); + + if ($path) { + $html = '' . $file['FILE_NAME'] . ' (' . $file['FILE_DESCRIPTION'] . ')' . ''; + } + + $row->AddViewField($this->code, $html); + } + } + /** - * Генерирует HTML для поля фильтрации - * @see AdminListHelper::createFilterForm(); - * @return mixed + * {@inheritdoc} */ - public function genFilterHTML() + public function showFilterHtml() { // TODO: Implement genFilterHTML() method. } + /** + * {@inheritdoc} + */ public function processEditAction() { - if (isset($_REQUEST['FIELDS_del'][$this->code . '_FILE']) AND $_REQUEST['FIELDS_del'][$this->code . '_FILE'] == 'Y') { - \CFile::Delete(intval($this->data[$this->code])); - $this->data[$this->code] = 0; - } - else if (isset($_REQUEST['FIELDS']['IMAGE_ID_FILE'])) - { - $name = $_FILES['FIELDS']['name'][$this->code.'_FILE']; - $path = $_REQUEST['FIELDS']['IMAGE_ID_FILE']; - $this->saveFile($name, $path); - } - else - { - $name = $_FILES['FIELDS']['name'][$this->code.'_FILE']; - $path = $_FILES['FIELDS']['tmp_name'][$this->code.'_FILE']; - $type = $_FILES['FIELDS']['type'][$this->code.'_FILE']; - $this->saveFile($name, $path, $type); + parent::processEditAction(); + + if ($this->getSettings('MULTIPLE')) { + if ($this->getSettings('READONLY') === true) { + //удаляем все добавленные файлы в режиме только для чтения + foreach ($this->data[$this->code] as $key => $value) { + if (!is_array($value)) { + unset($this->data[$this->code][$key]); + } + } + return false; + } + + if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) { + foreach ($this->data[$this->code] as $key => $value) { + if (is_array($value) && ($value['name'] || $value['tmp_name'])) { + $_FILES[$this->code]['name'][$key] = $value['name']; + $_FILES[$this->code]['type'][$key] = $value['type']; + $_FILES[$this->code]['tmp_name'][$key] = $value['tmp_name']; + $_FILES[$this->code]['error'][$key] = $value['error']; + $_FILES[$this->code]['size'][$key] = $value['size']; + unset($this->data[$this->code][$key]); + } else { + $_FILES[$this->code]['name'][$key] = ''; + } + } + if (!count($this->data[$this->code])) { + unset($this->data[$this->code]); + } + } + + if (!empty($_FILES[$this->getCode()])) { + foreach ($_FILES[$this->getCode()]['name'] as $key => $fileName) { + if (empty($fileName) + || empty($_FILES[$this->getCode()]['tmp_name'][$key]) + || !empty($_FILES[$this->getCode()]['error'][$key]) + ) { + if (isset($_REQUEST[$this->getCode() . '_del'][$key])) { + if (is_array($this->data[$this->getCode()][$key]) && + !empty($this->data[$this->getCode()][$key]['VALUE']) + ) { + \CFile::Delete(intval($this->data[$this->getCode()][$key]['VALUE'])); + } else { + \CFile::Delete(intval($this->data[$this->getCode()][$key])); + } + unset($this->data[$this->getCode()][$key]); + } elseif ($this->data[$this->getCode()][$key]['VALUE']) { + \CFile::UpdateDesc($this->data[$this->getCode()][$key]['VALUE'], + $_REQUEST[$this->getCode() . '_descr'][$key]); + } + continue; + } elseif (is_int($key)) { + //Удаляем старый файл при замене + if (is_array($this->data[$this->getCode()][$key]) && + !empty($this->data[$this->getCode()][$key]['VALUE']) + ) { + \CFile::Delete(intval($this->data[$this->getCode()][$key]['VALUE'])); + } else { + \CFile::Delete(intval($this->data[$this->getCode()][$key])); + } + } + + $description = null; + + if (isset($_REQUEST[$this->getCode() . '_descr'][$key])) { + $description = $_REQUEST[$this->getCode() . '_descr'][$key]; + } + + if (empty($this->data[$this->getCode()][$key])) { + unset($this->data[$this->getCode()][$key]); + } + + $fileId = $this->saveFile($fileName, $_FILES[$this->getCode()]['tmp_name'][$key], false, $description); + + if ($fileId) { + $this->data[$this->getCode()][$key] = array('VALUE' => $fileId); + } else { + $this->addError('DIGITALWAND_AH_FAIL_ADD_FILE', array( + 'FILE_NAME' => $_FILES[$this->getCode()]['name'][$key] + )); + } + } + } + } else { + if (class_exists('\Bitrix\Main\UI\FileInput', true) && $this->getSettings('IMAGE') === true) { + if (is_array($this->data[$this->code . '_FILE']) && ($this->data[$this->code . '_FILE']['name'] || + $this->data[$this->code . '_FILE']['tmp_name']) + ) { + $_FILES['FIELDS']['name'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['name']; + $_FILES['FIELDS']['type'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['type']; + $_FILES['FIELDS']['tmp_name'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['tmp_name']; + $_FILES['FIELDS']['error'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['error']; + $_FILES['FIELDS']['size'][$this->code . '_FILE'] = $this->data[$this->code . '_FILE']['size']; + } + } + + unset($this->data[$this->code . '_FILE']); + + if ($this->getSettings('READONLY') === true) { + return false; + } + + if (empty($_FILES['FIELDS']['name'][$this->code . '_FILE']) + || empty($_FILES['FIELDS']['tmp_name'][$this->code . '_FILE']) + || !empty($_FILES['FIELDS']['error'][$this->code . '_FILE']) + ) { + if (isset($_REQUEST['FIELDS_del'][$this->code . '_FILE']) AND $_REQUEST['FIELDS_del'][$this->code . '_FILE'] == 'Y') { + \CFile::Delete(intval($this->data[$this->code])); + $this->data[$this->code] = 0; + } elseif ($this->data[$this->code] && isset($_REQUEST['FIELDS_descr'][$this->code . '_FILE'])) { + \CFile::UpdateDesc($this->data[$this->code], + $_REQUEST['FIELDS_descr'][$this->code . '_FILE']); + } + return false; + } + + $description = null; + + if (isset($_REQUEST['FIELDS_descr'][$this->code . '_FILE'])) { + $description = $_REQUEST['FIELDS_descr'][$this->code . '_FILE']; + } + + $name = $_FILES['FIELDS']['name'][$this->code . '_FILE']; + $path = $_FILES['FIELDS']['tmp_name'][$this->code . '_FILE']; + $type = $_FILES['FIELDS']['type'][$this->code . '_FILE']; + + $this->saveFile($name, $path, $type, $description); } } - protected function saveFile($name, $path, $type = false) + protected function saveFile($name, $path, $type = false, $description = null) { - if (!$path) - { + if (!$path) { return false; } - - $fileInfo = \CFile::MakeFileArray( - $path, - $type - ); + $file = \CFile::MakeFileArray($path, $type); - if(!$fileInfo) return false; + if (!$file) { + return false; + } - $fileInfo["name"] = $name; - - /** @var AdminBaseHelper $model */ - $helper = $this->helper; - $fileId = \CFile::SaveFile($fileInfo, $helper::$module); + if (!empty($description)) { + $file['description'] = $description; + } - $code = $this->code; - if(isset($this->data[$code])) { - \CFile::Delete($this->data[$code]); + if ($this->getSettings('IMAGE') === true && stripos($file['type'], "image") === false) { + $this->addError('FILE_FIELD_TYPE_ERROR'); + + return false; } - if($this->getSettings('MULTIPLE')){ + $file['name'] = $name; + + $moduleId = $this->helper->getModule(); + $file['MODULE_ID'] = $moduleId; + + $fileId = \CFile::SaveFile($file, $moduleId); + if (!$this->getSettings('MULTIPLE')) { + $code = $this->code; + + if (isset($this->data[$code])) { + \CFile::Delete($this->data[$code]); + } + + $this->data[$code] = $fileId; } - $this->data[$code] = $fileId; - return true; + return $fileId; + } + + /** + * {@inheritdoc} + */ + protected function getValueReadonly() + { + $this->setSetting('UPLOAD', false); + $this->setSetting('MEDIALIB', false); + $this->setSetting('FILE_DIALOG', false); + $this->setSetting('CLOUD', false); + $this->setSetting('DELETE', false); + $this->setSetting('EDIT', false); + + return $this->getEditHtml(); } + /** + * {@inheritdoc} + */ + protected function getMultipleValueReadonly() + { + $this->setSetting('UPLOAD', false); + $this->setSetting('MEDIALIB', false); + $this->setSetting('FILE_DIALOG', false); + $this->setSetting('CLOUD', false); + $this->setSetting('DELETE', false); + $this->setSetting('EDIT', false); + + return $this->getMultipleEditHtml(); + } } \ No newline at end of file diff --git a/lib/widget/HLIBlockFieldWidget.php b/lib/widget/HLIBlockFieldWidget.php index bd8e564..26ab4ea 100644 --- a/lib/widget/HLIBlockFieldWidget.php +++ b/lib/widget/HLIBlockFieldWidget.php @@ -36,7 +36,7 @@ class HLIBlockFieldWidget extends HelperWidget * @see \CAdminForm::ShowUserFieldsWithReadyData * @return mixed */ - protected function genEditHTML() + protected function getEditHtml() { $info = $this->getUserFieldInfo(); if ($info) { @@ -193,7 +193,7 @@ public function getSettings($name = '') * * @return mixed */ - public function genListHTML(&$row, $data) + public function generateRow(&$row, $data) { $info = $this->getUserFieldInfo(); if ($info) { @@ -219,7 +219,7 @@ public function genListHTML(&$row, $data) * @see AdminListHelper::createFilterForm(); * @return mixed */ - public function genFilterHTML() + public function showFilterHtml() { $info = $this->getUserFieldInfo(); if ($info) { @@ -280,7 +280,8 @@ static public function getUserFields($iblockId, $data) /** * Заменяем оригинальную функцию, т.к. текст ошибки приходит от битрикса, причем название поля там почему-то не * проставлено - * @param string $messageId + * +*@param string $messageId */ protected function addError($messageId) { diff --git a/lib/widget/HelperWidget.php b/lib/widget/HelperWidget.php index ffce1f6..1e07a97 100644 --- a/lib/widget/HelperWidget.php +++ b/lib/widget/HelperWidget.php @@ -1,4 +1,5 @@ *
  • Отображение поля на странице редактирования
  • @@ -45,8 +40,10 @@ *
      *
    • HIDE_WHEN_CREATE - скрывает поле в форме редактирования, если создаётся новый элемент, а не открыт * существующий на редактирование.
    • - *
    • TITLE - название поля. Будет использовано в фильтре, заголовке таблицы и в качестве подписи поля на - * странице редактирования
    • + *
    • TITLE - название поля. Если не задано то возьмется значение title из DataManager::getMap() + * через getField($code)->getTitle(). Будет использовано в фильтре, заголовке таблицы и в качестве подписи поля + * на + * странице редактирования.
    • *
    • REQUIRED - является ли поле обязательным.
    • *
    • READONLY - поле нельзя редактировать, предназначено только для чтения
    • *
    • FILTER - позволяет указать способ фильтрации по полю. В базовом классе возможен только вариант "BETWEEN" @@ -59,12 +56,91 @@ * выводиться данные из нескольких полей сразу.
    • *
    • EDIT_IN_LIST - параметр не обрабатывается непосредственно виджетом, однако используется хэлпером. * Указывает, можно ли редактировать данное поле в спискке
    • + *
    • MULTIPLE - bool является ли поле множественным
    • + *
    • MULTIPLE_FIELDS - bool поля используемые в хранилище множественных значений и их алиасы
    • + *
    + * + * Как сделать виджет множественным? + *
      + *
    • Реализуйте метод genMultipleEditHTML(). Метод должен выводить множественную форму ввода. Для реализации формы + * ввода есть JS хелпер HelperWidget::jsHelper()
    • + *
    • Опишите поля, которые будут переданы связи в EntityManager. Поля описываются в настройке "MULTIPLE_FIELDS" + * виджета. По умолчанию множественный виджет использует поля ID, ENTITY_ID, VALUE
    • + *
    • Полученные от виджета данные будут переданы в EntityManager и сохранены как связанные данные
    • *
    + * Пример реализации можно увидеть в виджете StringWidget + * + * Как использовать множественный виджет? + *
      + *
    • + * Создайте таблицу и модель, которая будет хранить данные поля + * - Таблица обязательно должна иметь поля, которые требует виджет. + * Обязательные поля виджета по умолчанию описаны в: HelperWidget::$settings['MULTIPLE_FIELDS'] + * Если у виджета нестандартный набор полей, то они хранятся в: SomeWidget::$settings['MULTIPLE_FIELDS'] + * - Если поля, которые требует виджет есть в вашей таблице, но они имеют другие названия, + * можно настроить виджет для работы с вашими полями. + * Для этого переопределите настройку MULTIPLE_FIELDS при объявлении поля в интерфейсе следующим способом: + * ``` + * 'RELATED_LINKS' => array( + * 'WIDGET' => new StringWidget(), + * 'TITLE' => 'Ссылки', + * 'MULTIPLE' => true, + * // Обратите внимание, именно тут переопределяются поля виджета + * 'MULTIPLE_FIELDS' => array( + * 'ID', // Должны быть прописаны все поля, даже те, которые не нужно переопределять + * 'ENTITY_ID' => 'NEWS_ID', // ENTITY_ID - поле, которое требует виджет, NEWS_ID - пример поля, которое + * будет использоваться вместо ENTITY_ID + * 'VALUE' => 'LINK', // VALUE - поле, которое требует виджет, LINK - пример поля, которое будет + * использоваться вместо VALUE + * ) + * ), + * ``` + *
    • + * + *
    • + * Далее в основной модели (та, которая указана в AdminBaseHelper::$model) нужно прописать связь с моделью, + * в которой вы хотите хранить данные поля + * Пример объявления связи: + * ``` + * new Entity\ReferenceField( + * 'RELATED_LINKS', + * 'namespace\NewsLinksTable', + * array('=this.ID' => 'ref.NEWS_ID'), + * // Условия FIELD и ENTITY не обязательны, подробности смотрите в комментариях к классу @see EntityManager + * 'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'), + * 'ref.ENTITY' => new DB\SqlExpression('?s', 'news'), + * ), + * ``` + *
    • * - * @see HelperWidget::genEditHTML() - * @see HelperWidget::genListHTML() - * @see HelperWidget::genFilterHTML() + *
    • + * Что бы виджет работал во множественном режиме, нужно при описании интерфейса поля указать параметр MULTIPLE => true + * ``` + * 'RELATED_LINKS' => array( + * 'WIDGET' => new StringWidget(), + * 'TITLE' => 'Ссылки', + * // Включаем режим множественного ввода + * 'MULTIPLE' => true, + * ) + * ``` + *
    • + * + *
    • + * Готово :) + *
    • + *
    + * + * О том как сохраняются данные множественных виджетов можно узнать из комментариев + * класса \DigitalWand\AdminHelper\EntityManager. + * + * @see EntityManager + * @see HelperWidget::getEditHtml() + * @see HelperWidget::generateRow() + * @see showFilterHtml::showFilterHTML() * @see HelperWidget::setSetting() + * + * @author Nik Samokhvalov + * @author Dmitriy Baibuhtin */ abstract class HelperWidget { @@ -72,87 +148,97 @@ abstract class HelperWidget const EDIT_HELPER = 2; /** - * @var string - * Название поля ("символьный код") + * @var string Код поля. */ protected $code; - /** - * @var array $settings - * Настройки виджета для данной модели + * @var array $settings Настройки виджета для данной модели. */ - protected $settings = array(); - + protected $settings = array( + // Поля множественного виджета по умолчанию (array('ОРИГИНАЛЬНОЕ НАЗВАНИЕ', 'ОРИГИНАЛЬНОЕ НАЗВАНИЕ' => 'АЛИАС')) + 'MULTIPLE_FIELDS' => array('ID', 'VALUE', 'ENTITY_ID') + ); /** - * @var array - * Настройки "по-умолчанию" для виджета + * @var array Настройки "по-умолчанию" для модели. */ - static protected $defaults = array(); - + static protected $defaults; /** - * @var DataManager - * Название класса модели + * @var DataManager Название класса модели. */ protected $entityName; - /** - * @var array - * Данные модели + * @var array Данные модели. */ protected $data; - - /** @var AdminBaseHelper|AdminListHelper|AdminEditHelper $helper - * Экземпляр хэлпера, вызывающий данный виджет + /** @var AdminBaseHelper|AdminListHelper|AdminEditHelper $helper Экземпляр хэлпера, вызывающий данный виджет. */ protected $helper; - /** - * @var array $validationErrors - * Ошибки валидации поля + * @var bool Статус отображения JS хелпера. Используется для исключения дублирования JS-кода. + */ + protected $jsHelper = false; + /** + * @var array $validationErrors Ошибки валидации поля. */ protected $validationErrors = array(); - /** - * @var string - * Строка, добавляемая к полю name полей фильтра + * @var string Строка, добавляемая к полю name полей фильтра. */ protected $filterFieldPrefix = 'find_'; /** - * @param array $settings * Эксемпляр виджета создаётся всего один раз, при описании настроек интерфейса. При создании есть возможность - * сразу указать для него необходимые настройки + * сразу указать для него необходимые настройки. + * + * @param array $settings */ - public function __construct($settings = array()) + public function __construct(array $settings = array()) { + Loc::loadMessages(__FILE__); + $this->settings = $settings; } /** - * Генерирует HTML для редактирования поля + * Генерирует HTML для редактирования поля. + * + * @return string + * + * @api + */ + abstract protected function getEditHtml(); + + /** + * Генерирует HTML для редактирования поля в мульти-режиме. * * @return string + * * @api */ - abstract protected function genEditHTML(); + protected function getMultipleEditHtml() + { + return Loc::getMessage('DIGITALWAND_AH_MULTI_NOT_SUPPORT'); + } /** - * Оборачивает поле в HTML код, который в большинстве случаев менять не придется. - * Далее вызывается кастомизируемая часть. + * Оборачивает поле в HTML код, который в большинстве случаев менять не придется. Далее вызывается + * кастомизируемая часть. * - * @param $isPKField - является ли поле первичным ключом модели + * @param bool $isPKField Является ли поле первичным ключом модели. * - * @see HelperWidget::genEditHTML(); + * @see HelperWidget::getEditHtml(); */ - public function genBasicEditField($isPKField) + public function showBasicEditField($isPKField) { if ($this->getSettings('HIDE_WHEN_CREATE') AND !isset($this->data['ID'])) { return; } - if ($this->getSettings('USE_BX_API')) { - $this->genEditHTML(); + // JS хелперы + $this->jsHelper(); + if ($this->getSettings('USE_BX_API')) { + $this->getEditHtml(); } else { print ''; $title = $this->getSettings('TITLE'); @@ -162,6 +248,7 @@ public function genBasicEditField($isPKField) print '' . $title . ':'; $field = $this->getValue(); + if (is_null($field)) { $field = ''; } @@ -169,11 +256,18 @@ public function genBasicEditField($isPKField) $readOnly = $this->getSettings('READONLY'); if (!$readOnly AND !$isPKField) { - $field = $this->genEditHTML(); - + if ($this->getSettings('MULTIPLE')) { + $field = $this->getMultipleEditHtml(); + } else { + $field = $this->getEditHtml(); + } } else { if ($readOnly) { - $field = $this->getValueReadonly(); + if ($this->getSettings('MULTIPLE')) { + $field = $this->getMultipleValueReadonly(); + } else { + $field = $this->getValueReadonly(); + } } } @@ -183,43 +277,149 @@ public function genBasicEditField($isPKField) } /** - * Возвращает значение поля в форме "только для чтения". + * Возвращает значение поля в форме "только для чтения" для не множественных свойств. * * @return mixed */ protected function getValueReadonly() { - return $this->getValue(); + return static::prepareToOutput($this->getValue()); + } + + /** + * Возвращает значения множественного поля. + * + * @return array + */ + protected function getMultipleValue() + { + $rsEntityData = null; + $values = array(); + if (!empty($this->data['ID'])) { + $entityName = $this->entityName; + $rsEntityData = $entityName::getList(array( + 'select' => array('REFERENCE_' => $this->getCode() . '.*'), + 'filter' => array('=ID' => $this->data['ID']) + )); + + if ($rsEntityData) { + while ($referenceData = $rsEntityData->fetch()) { + if (empty($referenceData['REFERENCE_' . $this->getMultipleField('ID')])) { + continue; + } + $values[] = $referenceData['REFERENCE_' . $this->getMultipleField('VALUE')]; + } + } + } else { + if ($this->data[$this->code]) { + $values = $this->data[$this->code]; + } + } + + return $values; + } + + /** + * Возвращает значение поля в форме "только для чтения" для множественных свойств. + * + * @return string + */ + protected function getMultipleValueReadonly() + { + $values = $this->getMultipleValue(); + + foreach ($values as &$value) { + $value = static::prepareToOutput($value); + } + + return join('
    ', $values); } /** - * Генерирует HTML для поля в списке + * Обработка строки для безопасного отображения. Если нужно отобразить текст как аттрибут тега, + * используйте static::prepareToTag(). * - * @see AdminListHelper::addRowCell(); + * @param string $string + * @param bool $hideTags Скрыть теги: + * + * - true - вырезать теги оставив содержимое. Результат обработки: text = text + * + * - false - отобразаить теги в виде текста. Результат обработки: text = <b>text</b> + * + * @return string + */ + public static function prepareToOutput($string, $hideTags = true) + { + if ($hideTags) { + return preg_replace('/<.+>/mU', '', $string); + } else { + return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); + } + } + + /** + * Подготовка строки для использования в аттрибутах тегов. Например: + * ``` + * + * ``` + * + * @param string $string + * + * @return string + */ + public static function prepareToTagAttr($string) + { + // Не используйте addcslashes в этом методе, иначе в тегах будут дубли обратных слешей + return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); + } + + /** + * Подготовка строки для использования в JS. + * + * @param string $string + * + * @return string + */ + public static function prepareToJs($string) + { + $string = htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); + $string = addcslashes($string, "\r\n\"\\"); + + return $string; + } + + /** + * Генерирует HTML для поля в списке. * * @param \CAdminListRow $row - * @param array $data - данные текущей строки + * @param array $data Данные текущей строки. * * @return void + * + * @see AdminListHelper::addRowCell() + * * @api */ - abstract public function genListHTML(&$row, $data); + abstract public function generateRow(&$row, $data); /** - * Генерирует HTML для поля фильтрации + * Генерирует HTML для поля фильтрации. * - * @see AdminListHelper::createFilterForm(); * @return void + * + * @see AdminListHelper::createFilterForm() + * * @api */ - abstract public function genFilterHTML(); + abstract public function showFilterHtml(); /** - * Возвращает массив настроек данного виджета, либо значение отдельного параметра, если указано его имя + * Возвращает массив настроек данного виджета, либо значение отдельного параметра, если указано его имя. * - * @param string $name название конкретного параметра + * @param string $name Название конкретного параметра. * * @return array|mixed + * * @api */ public function getSettings($name = '') @@ -241,6 +441,7 @@ public function getSettings($name = '') /** * Передаёт в виджет ссылку на вызывающий его объект. + * * @param AdminBaseHelper $helper */ public function setHelper(&$helper) @@ -249,7 +450,7 @@ public function setHelper(&$helper) } /** - * Возвращает текукщее значение поля фильтрации (спец. символы экранированы) + * Возвращает текукщее значение поля фильтрации (спец. символы экранированы). * * @return bool|string */ @@ -318,15 +519,17 @@ public function changeGetListOptions(&$filter, &$select, &$sort, $raw) } } } - - } else if ($filterPrefix = $this->getSettings('FILTER') AND $filterPrefix !== true AND isset($filter[$this->getCode()])) { - $filter[$filterPrefix . $this->getCode()] = $filter[$this->getCode()]; - unset($filter[$this->getCode()]); + } else { + if ($filterPrefix = $this->getSettings('FILTER') AND $filterPrefix !== true AND isset($filter[$this->getCode()])) { + $filter[$filterPrefix . $this->getCode()] = $filter[$this->getCode()]; + unset($filter[$this->getCode()]); + } } } /** - * Проверяет оператор фильтрации + * Проверяет оператор фильтрации. + * * @return bool */ protected function isFilterBetween() @@ -344,10 +547,10 @@ protected function isFilterBetween() public function processEditAction() { if (!$this->checkRequired()) { - $this->addError('REQUIRED_FIELD_ERROR'); + $this->addError('DIGITALWAND_AH_REQUIRED_FIELD_ERROR'); } if ($this->getSettings('UNIQUE') && !$this->isUnique()) { - $this->addError('DUPLICATE_FIELD_ERROR'); + $this->addError('DIGITALWAND_AH_DUPLICATE_FIELD_ERROR'); } } @@ -361,12 +564,19 @@ public function processAfterSaveAction() /** * Добавляет строку ошибки в массив ошибок. - * @param string $messageId сообщение об ошибке. Плейсхолдер #FIELD# будет заменён на значение параметра TITLE + * + * @param string $messageId Код сообщения об ошибке из лэнг-файла. Плейсхолдер #FIELD# будет заменён на значение + * параметра TITLE. + * @param array $replace Данные для замены. + * * @see Loc::getMessage() */ - protected function addError($messageId) + protected function addError($messageId, $replace = array()) { - $this->validationErrors[$this->getCode()] = Loc::getMessage($messageId, array('#FIELD#' => $this->getSettings('TITLE'))); + $this->validationErrors[$this->getCode()] = Loc::getMessage( + $messageId, + array_merge(array('#FIELD#' => $this->getSettings('TITLE')), $replace) + ); } /** @@ -387,8 +597,9 @@ public function checkRequired() } /** - * @param string $code * Выставляет код для данного виджета при инициализации. Перегружает настройки. + * + * @param string $code */ public function setCode($code) { @@ -405,14 +616,14 @@ public function getCode() } /** - * Устанавливает настройки интерфейса для текущего поля - * - * @see AdminBaseHelper::getInterfaceSettings(); - * @see AdminBaseHelper::setFields(); + * Устанавливает настройки интерфейса для текущего поля. * * @param string $code * * @return bool + * + * @see AdminBaseHelper::getInterfaceSettings() + * @see AdminBaseHelper::setFields() */ public function loadSettings($code = null) { @@ -425,14 +636,14 @@ public function loadSettings($code = null) } unset($interface['FIELDS'][$code]['WIDGET']); $this->settings = array_merge($this->settings, $interface['FIELDS'][$code]); - $this->settings = array_merge(static::$defaults, $this->settings); $this->setDefaultValue(); return true; } /** - * Возвращает название сущности данной модели + * Возвращает название сущности данной модели. + * * @return string|DataManager */ public function getEntityName() @@ -461,15 +672,14 @@ public function setDefaultValue() /** * Передает ссылку на данные сущности в виджет + * * @param $data */ public function setData(&$data) { $this->data = &$data; //FIXME: нелепый оверхэд ради того, чтобы можно было централизованно преобразовывать значение при записи - if(isset($data[$this->getCode()])){ - $this->setValue($data[$this->getCode()]); - } + $this->setValue($data[$this->getCode()]); } /** @@ -487,7 +697,9 @@ public function getValue() /** * Устанавливает значение поля + * * @param $value + * * @return bool */ protected function setValue($value) @@ -498,8 +710,38 @@ protected function setValue($value) return true; } + /** + * Получения названия поля таблицы, в которой хранятся множественные данные этого виджета + * + * @param string $fieldName Название поля + * + * @return bool|string + */ + public function getMultipleField($fieldName) + { + $fields = $this->getSettings('MULTIPLE_FIELDS'); + if (empty($fields)) { + return $fieldName; + } + + // Поиск алиаса названия поля + if (isset($fields[$fieldName])) { + return $fields[$fieldName]; + } + + // Поиск оригинального названия поля + $fieldsFlip = array_flip($fields); + + if (isset($fieldsFlip[$fieldName])) { + return $fieldsFlip[$fieldName]; + } + + return $fieldName; + } + /** * Выставляет значение отдельной настройки + * * @param string $name * @param mixed $value */ @@ -531,7 +773,6 @@ protected function getFilterInputName() $inputNameTo = $baseName . '_to'; return array($inputNameFrom, $inputNameTo); - } else { return $this->filterFieldPrefix . $this->code; } @@ -539,6 +780,7 @@ protected function getFilterInputName() /** * Возвращает текст для атрибута name инпута редактирования. + * * @param null $suffix опциональное дополнение к названию поля * * @return string @@ -548,6 +790,17 @@ protected function getEditInputName($suffix = null) return 'FIELDS[' . $this->getCode() . $suffix . ']'; } + /** + * Уникальный ID для DOM HTML + * @return string + */ + protected function getEditInputHtmlId() + { + $htmlId = end(explode('\\', $this->entityName)) . '-' . $this->getCode(); + + return strtolower(preg_replace('/[^A-z-]/', '-', $htmlId)); + } + /** * Возвращает текст для атрибута name инпута редактирования поля в списке * @return string @@ -568,10 +821,10 @@ protected function getEditableListInputName() */ protected function getCurrentViewType() { - if (is_a($this->helper, 'AdminListHelper')) { + if (is_a($this->helper, 'DigitalWand\AdminHelper\Helper\AdminListHelper')) { return self::LIST_HELPER; } else { - if (is_a($this->helper, 'AdminEditHelper')) { + if (is_a($this->helper, 'DigitalWand\AdminHelper\Helper\AdminEditHelper')) { return self::EDIT_HELPER; } } @@ -629,4 +882,163 @@ protected function isExcelView() return false; } + + /** + * @todo Вынести в ресурс (\CJSCore::Init()). + * @todo Описать. + */ + protected function jsHelper() + { + if ($this->jsHelper == true) { + return true; + } + + $this->jsHelper = true; + \CJSCore::Init(array("jquery")); + ?> + + + *
  • IBLOCK_ID - (int) ID инфоблока + *
  • INPUT_SIZE - (int) значение атрибута size для input
  • + *
  • WINDOW_WIDTH - (int) значение width для всплывающего окна выбора элемента
  • + *
  • WINDOW_HEIGHT - (int) значение height для всплывающего окна выбора элемента
  • + * + * + * @author Nik Samokhvalov + */ +class IblockElementWidget extends NumberWidget +{ + static protected $defaults = array( + 'FILTER' => '=', + 'INPUT_SIZE' => 5, + 'WINDOW_WIDTH' => 600, + 'WINDOW_HEIGHT' => 500, + ); + + public function __construct(array $settings = array()) + { + Loc::loadMessages(__FILE__); + Loader::includeModule('iblock'); + + parent::__construct($settings); + } + + /** + * {@inheritdoc} + */ + public function getEditHtml() + { + $iblockId = (int) $this->getSettings('IBLOCK_ID'); + $inputSize = (int) $this->getSettings('INPUT_SIZE'); + $windowWidth = (int) $this->getSettings('WINDOW_WIDTH'); + $windowHeight = (int) $this->getSettings('WINDOW_HEIGHT'); + + $name = 'FIELDS'; + $key = $this->getCode(); + + $elementId = $this->getValue(); + + if (!empty($elementId)) { + $rsElement = ElementTable::getById($elementId); + + if (!$element = $rsElement->fetchAll()) { + $element['NAME'] = Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); + } + } else { + $elementId = ''; + } + + return '' . + '' . ' ' + . static::prepareToOutput($element['NAME']) + . ''; + } + + /** + * {@inheritdoc} + */ + public function getValueReadonly() + { + $elementId = $this->getValue(); + + if (!empty($elementId)) { + $rsElement = ElementTable::getById($elementId); + $element = $rsElement->fetch(); + + return '[' . $elementId . '] ' . static::prepareToOutput($element['NAME']) . ''; + } + } + + /** + * {@inheritdoc} + */ + public function generateRow(&$row, $data) + { + $elementId = $this->getValue(); + + if (!empty($elementId)) { + $rsElement = ElementTable::getById($elementId); + $element = $rsElement->fetch(); + + $html = '[' . $elementId . '] ' . static::prepareToOutput($element['NAME']) . ''; + } else { + $html = ''; + } + + $row->AddViewField($this->getCode(), $html); + } +} \ No newline at end of file diff --git a/lib/widget/ImageWidget.php b/lib/widget/ImageWidget.php deleted file mode 100644 index 675c4e7..0000000 --- a/lib/widget/ImageWidget.php +++ /dev/null @@ -1,128 +0,0 @@ - 100, - 'LIST_HEIGHT' => 100, - 'LIST_FILTERS' => false, - 'LIST_QUALITY' => 80, - 'FILTER' => false - ); - /** - * Генерирует HTML для редактирования поля - * @return mixed - */ - protected function genEditHTML() - { - return \CFileInput::Show($this->getEditInputName('_FILE'), ($this->getValue() > 0 ? $this->getValue() : 0), - array( - "IMAGE" => "Y", - "PATH" => "Y", - "FILE_SIZE" => "Y", - "DIMENSIONS" => "Y", - "IMAGE_POPUP" => "Y", - "MAX_SIZE" => array( - "W" => 100, - "H" => 100, - ), - "ALLOW_UPLOAD" => "I", - ), array( - 'upload' => true, - 'medialib' => false, - 'file_dialog' => false, - 'cloud' => false, - 'del' => true, - 'description' => false, - ) - ); - } - - /** - * Генерирует HTML для поля в списке - * @see AdminListHelper::addRowCell(); - * @param \CAdminListRow $row - * @param array $data - данные текущей строки - * @return mixed - */ - public function genListHTML(&$row, $data) - { - if ($_REQUEST['mode'] == 'excel') { - $path = \CFile::GetPath($data[$this->code]); - $scheme = empty($_SERVER['REQUEST_SCHEME']) ? 'http' : $_SERVER['REQUEST_SCHEME']; - $html = $scheme . '://' . $_SERVER['SERVER_NAME'] . $path; - - } else { - $image = $this->getImageByID($data[$this->code], 'LIST'); - if (!$image) { - $html = ""; - } else { - $html = ''; - } - } - - $row->AddViewField($this->code, $html); - - } - - private function getImageByID($id, $resizeFor) - { - $size['width'] = intval($this->getSettings($resizeFor . '_WIDTH')); - $size['height'] = intval($this->getSettings($resizeFor . '_HEIGHT')); - - $filters = $this->getSettings($resizeFor . '_FILTERS'); - $quality = $this->getSettings($resizeFor . '_QUALITY'); - - return \CFile::ResizeImageGet($id, $size, BX_RESIZE_IMAGE_EXACT, true, $filters, false, $quality); - - } - - protected function saveFile($name, $path, $type = false) - { - if (!$path) - { - return false; - } - - $fileInfo = \CFile::MakeFileArray( - $path, - $type - ); - - - if(!$fileInfo) return false; - - if (stripos($fileInfo['type'], "image") === false) - { - $this->addError('FILE_FIELD_TYPE_ERROR'); - return false; - } - - $fileInfo["name"] = $name; - - /** @var AdminBaseHelper $model */ - $helper = $this->helper; - $fileId = \CFile::SaveFile($fileInfo, $helper::$module); - - $code = $this->code; - if(isset($this->data[$code])) { - \CFile::Delete($this->data[$code]); - } - - if($this->getSettings('MULTIPLE')){ - - } - - $this->data[$code] = $fileId; - return true; - } - - -} \ No newline at end of file diff --git a/lib/widget/NumberWidget.php b/lib/widget/NumberWidget.php index deda5ac..02eeb76 100644 --- a/lib/widget/NumberWidget.php +++ b/lib/widget/NumberWidget.php @@ -7,15 +7,13 @@ Loc::loadMessages(__FILE__); /** - * Class NumberWidget - * Виджет с числовыми значениями - * Точная копия StringWidget, только работает с числами и не ищет по подстроке + * Виджет с числовыми значениями. Точная копия StringWidget, только работает с числами и не ищет по подстроке. */ class NumberWidget extends StringWidget { - static protected $defaults = array( - 'FILTER' => '=' + 'FILTER' => '=', + 'EDIT_IN_LIST' => true ); public function checkFilter($operationType, $value) diff --git a/lib/widget/OrmElementWidget.php b/lib/widget/OrmElementWidget.php new file mode 100644 index 0000000..6320600 --- /dev/null +++ b/lib/widget/OrmElementWidget.php @@ -0,0 +1,428 @@ + + */ +class OrmElementWidget extends NumberWidget +{ + protected static $defaults = array( + 'FILTER' => '=', + 'INPUT_SIZE' => 5, + 'WINDOW_WIDTH' => 600, + 'WINDOW_HEIGHT' => 500, + 'TITLE_FIELD_NAME' => 'TITLE', + 'TEMPLATE' => 'select', + 'ADDITIONAL_URL_PARAMS' => array() + ); + + /** + * @inheritdoc + */ + public function loadSettings($code = null) + { + $load = parent::loadSettings($code); + + if (!is_subclass_of($this->getSettings('HELPER'), '\DigitalWand\AdminHelper\Helper\AdminBaseHelper')) + { + throw new ArgumentTypeException('HELPER', '\DigitalWand\AdminHelper\Helper\AdminBaseHelper'); + } + + if (!is_array($this->getSettings('ADDITIONAL_URL_PARAMS'))) + { + throw new ArgumentTypeException('ADDITIONAL_URL_PARAMS', 'array'); + } + + return $load; + } + + /** + * @inheritdoc + */ + public function getEditHtml() + { + if ($this->getSettings('TEMPLATE') == 'radio') { + $html = $this->genEditHtmlInputs(); + } else { + $html = $this->getEditHtmlSelect(); + } + + return $html; + } + + /** + * Генерирует HTML с выбором элемента во вcплывающем окне, шаблон select. + * + * @return string + */ + protected function getEditHtmlSelect() + { + /** + * @var AdminBaseHelper $linkedHelper + */ + $linkedHelper = $this->getSettings('HELPER'); + $inputSize = (int) $this->getSettings('INPUT_SIZE'); + $windowWidth = (int) $this->getSettings('WINDOW_WIDTH'); + $windowHeight = (int) $this->getSettings('WINDOW_HEIGHT'); + + $name = 'FIELDS'; + $key = $this->getCode(); + + $entityData = $this->getOrmElementData(); + + if (!empty($entityData)) { + $elementId = $entityData['ID']; + $elementName = $entityData[$this->getSettings('TITLE_FIELD_NAME')] ? + $entityData[$this->getSettings('TITLE_FIELD_NAME')] : + Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); + } else { + $elementId = ''; + } + + $popupUrl = $linkedHelper::getUrl(array_merge( + array( + 'popup' => 'Y', + 'eltitle' => $this->getSettings('TITLE_FIELD_NAME'), + 'n' => $name, + 'k' => $key + ), + $this->getSettings('ADDITIONAL_URL_PARAMS') + )); + + return '' . + '' . ' ' . + static::prepareToOutput($elementName) + . ''; + } + + /** + * Генерирует HTML с выбором элемента в виде радио инпутов. + * + * @return string + */ + public function genEditHtmlInputs() + { + $return = ''; + + $elementList = $this->getOrmElementList(); + + if (!is_null($elementList)) { + foreach ($elementList as $key => $element) { + $return .= InputType("radio", $this->getEditInputName(), $element['ID'], $this->getValue(), false, $element['TITLE']); + } + } else { + $return = 'Элементы не найдены'; + } + + return $return; + } + + /** + * @inheritdoc + */ + public function getMultipleEditHtml() + { + /** + * @var AdminBaseHelper $linkedHelper + */ + $linkedHelper = $this->getSettings('HELPER'); + $inputSize = (int)$this->getSettings('INPUT_SIZE'); + $windowWidth = (int)$this->getSettings('WINDOW_WIDTH'); + $windowHeight = (int)$this->getSettings('WINDOW_HEIGHT'); + + $name = 'FIELDS'; + $key = $this->getCode(); + + $uniqueId = $this->getEditInputHtmlId(); + + $entityListData = $this->getOrmElementData(); + + $popupUrl = $linkedHelper::getUrl(array_merge( + array( + 'popup' => 'Y', + 'eltitle' => $this->getSettings('TITLE_FIELD_NAME'), + 'n' => $name, + 'k' => '{{field_id}}' + ), + $this->getSettings('ADDITIONAL_URL_PARAMS') + )); + + ob_start(); + ?> + +
    + + + getOrmElementData(); + + if (!empty($entityData)) { + $entityName = $entityData[$this->getSettings('TITLE_FIELD_NAME')] ? + $entityData[$this->getSettings('TITLE_FIELD_NAME')] : + Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); + + return '[' . $entityData['ID'] . ']' . static::prepareToOutput($entityName); + } + + return ''; + } + + /** + * @inheritdoc + */ + public function getMultipleValueReadonly() + { + $entityListData = $this->getOrmElementData(); + + if (!empty($entityListData)) { + $multipleData = array(); + + foreach ($entityListData as $entityData) { + $entityName = $entityData[$this->getSettings('TITLE_FIELD_NAME')] ? + $entityData[$this->getSettings('TITLE_FIELD_NAME')] : + Loc::getMessage('IBLOCK_ELEMENT_NOT_FOUND'); + + $multipleData[] = '[' . $entityData['ID'] . ']' . static::prepareToOutput($entityName); + } + + return implode('
    ', $multipleData); + } + + return ''; + } + + /** + * @inheritdoc + */ + public function generateRow(&$row, $data) + { + if ($this->getSettings('MULTIPLE')) { + $strElement = $this->getMultipleValueReadonly(); + } else { + $strElement = $this->getValueReadonly(); + } + + $row->AddViewField($this->getCode(), $strElement); + } + + /** + * @inheritdoc + */ + public function showFilterHtml() + { + /** + * @var AdminBaseHelper $linkedHelper + */ + $linkedHelper = $this->getSettings('HELPER'); + + if ($this->getSettings('MULTIPLE')) { + + } else { + $inputSize = (int) $this->getSettings('INPUT_SIZE'); + $windowWidth = (int) $this->getSettings('WINDOW_WIDTH'); + $windowHeight = (int) $this->getSettings('WINDOW_HEIGHT'); + + $name = 'FIND'; + $key = $this->getCode(); + + print ''; + print '' . $this->getSettings('TITLE') . ''; + + $popupUrl = $linkedHelper::getUrl(array_merge( + array( + 'popup' => 'Y', + 'eltitle' => $this->getSettings('TITLE_FIELD_NAME'), + 'n' => $name, + 'k' => $key + ), + $this->getSettings('ADDITIONAL_URL_PARAMS') + )); + + $editStr = '' . + ''; + + print '' . $editStr . ''; + + print ''; + } + } + + /** + * Получает информацию о записях, к которым осуществлена привязка. + * + * @return array + * @throws \Bitrix\Main\ArgumentException + */ + protected function getOrmElementData() + { + $refInfo = array(); + $valueList = null; + $linkedModel = $this->getLinkedModel(); + + if ($this->getSettings('MULTIPLE')) { + $entityName = $this->entityName; + + $rsMultEntity = $entityName::getList(array( + 'select' => array('REFERENCE_' => $this->getCode() . '.*'), + 'filter' => array('=ID' => $this->data['ID']) + )); + + while ($multEntity = $rsMultEntity->fetch()) { + $valueList[$multEntity['REFERENCE_VALUE']] = $multEntity['REFERENCE_VALUE']; + } + } else { + $value = $this->getValue(); + + if (!empty($value)) { + $valueList[$value] = $value; + } + } + + if ($valueList) { + $rsEntity = $linkedModel::getList(array( + 'filter' => array('ID' => $valueList) + )); + + while ($entity = $rsEntity->fetch()) { + if (in_array($entity['ID'], $valueList)) { + unset($valueList[$entity['ID']]); + } + + if ($this->getSettings('MULTIPLE')) { + $refInfo[] = $entity; + } else { + $refInfo = $entity; + break; + } + } + + foreach ($valueList as $entityId) { + if ($this->getSettings('MULTIPLE')) { + $refInfo[] = array('ID' => $entityId); + } else { + $refInfo = array('ID' => $entityId); + break; + } + } + } + + return $refInfo; + } + + /** + * Получает информацию о всех активных элементах для их выбора в виджете. + * + * @return array + * + * @throws \Bitrix\Main\ArgumentException + */ + protected function getOrmElementList() + { + $valueList = null; + $linkedModel = $this->getLinkedModel(); + + $rsEntity = $linkedModel::getList(array( + 'filter' => array( + 'ACTIVE' => 1 + ), + 'select' => array( + 'ID', + 'TITLE' + ) + )); + + while ($entity = $rsEntity->fetch()) { + $valueList[] = $entity; + } + + return $valueList; + } + + /** + * Возвращает связанную модель. + * + * @return \Bitrix\Main\Entity\DataManager + */ + protected function getLinkedModel() + { + /** + * @var \DigitalWand\AdminHelper\Helper\AdminBaseHelper $linkedHelper + */ + $linkedHelper = $this->getSettings('HELPER'); + + return $linkedHelper::getModel(); + } +} \ No newline at end of file diff --git a/lib/widget/StringWidget.php b/lib/widget/StringWidget.php index 772f4fd..19ea069 100644 --- a/lib/widget/StringWidget.php +++ b/lib/widget/StringWidget.php @@ -3,93 +3,202 @@ namespace DigitalWand\AdminHelper\Widget; use Bitrix\Main\Localization\Loc; +use DigitalWand\AdminHelper\Helper\AdminEditHelper; +use DigitalWand\AdminHelper\Helper\AdminListHelper; +use DigitalWand\AdminHelper\Helper\AdminSectionListHelper; Loc::loadMessages(__FILE__); /** - * Class StringWidget * Виджет строки с текстом. * * Доступные опции: *
      + *
    • EDIT_LINK - отображать в виде ссылки на редактирование элемента
    • *
    • STYLE - inline-стили для input
    • *
    • SIZE - значение атрибута size для input
    • *
    • TRANSLIT - true, если поле будет транслитерироваться в символьный код
    • + *
    • MULTIPLE - поддерживается множественный ввод. В таблице требуется наличие поля VALUE
    • *
    */ class StringWidget extends HelperWidget { - static protected $defaults = array( 'FILTER' => '%', //Фильтрация по подстроке, а не по точному соответствию. 'EDIT_IN_LIST' => true ); /** - * Генерирует HTML для редактирования поля - * @return mixed + * @inheritdoc */ - protected function genEditHTML() + protected function getEditHtml() { $style = $this->getSettings('STYLE'); $size = $this->getSettings('SIZE'); $link = ''; + if ($this->getSettings('TRANSLIT')) { //TODO: refactor this! $uniqId = get_class($this->entityName) . '_' . $this->getCode(); $nameId = 'name_link_' . $uniqId; $linkedFunctionName = 'set_linked_' . get_class($this->entityName) . '_CODE';//FIXME: hardcode here!!! + if (isset($this->entityName->{$this->entityName->pk()})) { $pkVal = $this->entityName->{$this->entityName->pk()}; } else { $pkVal = '_new_'; } + $nameId .= $pkVal; $linkedFunctionName .= $pkVal; $link = ''; } - //FIXME: тут было htmlentities, на на этом проекте оно превращает кириллицу в квакозябры. return '' . $link; } + protected function getMultipleEditHtml() + { + $style = $this->getSettings('STYLE'); + $size = $this->getSettings('SIZE'); + $uniqueId = $this->getEditInputHtmlId(); + + $rsEntityData = null; + + if (!empty($this->data['ID'])) { + $entityName = $this->entityName; + $rsEntityData = $entityName::getList(array( + 'select' => array('REFERENCE_' => $this->getCode() . '.*'), + 'filter' => array('=ID' => $this->data['ID']) + )); + } + + ob_start(); + ?> + +
    +
    + + + data['ID'])) { + $entityName = $this->entityName; + $rsEntityData = $entityName::getList(array( + 'select' => array('REFERENCE_' => $this->getCode() . '.*'), + 'filter' => array('=ID' => $this->data['ID']) + )); + } + + $result = ''; + if ($rsEntityData) { + while ($referenceData = $rsEntityData->fetch()) { + if (empty($referenceData['REFERENCE_VALUE'])) { + continue; + } + + $result .= '
    ' . + static::prepareToOutput($referenceData['REFERENCE_VALUE']) . '
    '; + } + } + + return $result; + } + /** * Генерирует HTML для поля в списке * @see AdminListHelper::addRowCell(); * @param \CAdminListRow $row * @param array $data - данные текущей строки */ - public function genListHTML(&$row, $data) + public function generateRow(&$row, $data) { - $value = $this->getValue(); - if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { - $row->AddInputField($this->getCode(), array('style' => 'width:90%')); - } - $row->AddViewField($this->getCode(), $value); + if ($this->getSettings('MULTIPLE')) { + } else { + if ($this->getSettings('EDIT_LINK') || $this->getSettings('SECTION_LINK')) { + $entityClass = $this->entityName; + $pk = $entityClass::getEntity()->getPrimary(); + + if ($this->getSettings('SECTION_LINK')) { + $params = $this->helper->isPopup() ? $_GET : array(); + $params['ID'] = $this->data[$pk]; + $listHelper = $this->helper->getHelperClass($this->helper->isPopup() ? AdminSectionListHelper::className() : AdminListHelper::className()); + $pageUrl = $listHelper::getUrl($params); + $value = ''; + } else { + $editHelper = $this->helper->getHelperClass(AdminEditHelper::className()); + $pageUrl = $editHelper::getUrl(array( + 'ID' => $this->data[$pk] + )); + } + + $value .= '' . static::prepareToOutput($this->getValue()) . ''; + } else { + $value = static::prepareToOutput($this->getValue()); + } + if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { + $row->AddInputField($this->getCode(), array('style' => 'width:90%')); + } + + $row->AddViewField($this->getCode(), $value); + } } /** - * Генерирует HTML для поля фильтрации - * Если это BETWEEN, то выводит два поля для фильтрации - * @see AdminListHelper::createFilterForm(); - * @return mixed + * @inheritdoc */ - public function genFilterHTML() + public function showFilterHtml() { - print ''; - print '' . $this->getSettings('TITLE') . ''; + if ($this->getSettings('MULTIPLE')) { + } else { + print ''; + print '' . $this->getSettings('TITLE') . ''; - if ($this->isFilterBetween()) { - list($from, $to) = $this->getFilterInputName(); - print ' + if ($this->isFilterBetween()) { + list($from, $to) = $this->getFilterInputName(); + print '
    От:
    @@ -101,11 +210,11 @@ public function genFilterHTML()
    '; + } else { + print ''; + } - } else { - print ''; - + print ''; } - print ''; } } \ No newline at end of file diff --git a/lib/widget/TextAreaWidget.php b/lib/widget/TextAreaWidget.php index 3e85d04..f8d50cf 100644 --- a/lib/widget/TextAreaWidget.php +++ b/lib/widget/TextAreaWidget.php @@ -1,4 +1,5 @@ getSettings('COLS'); $rows = $this->getSettings('ROWS'); - return ''; + + return ''; } /** - * Генерирует HTML для поля в списке - * @see AdminListHelper::addRowCell(); - * @param \CAdminListRow $row - * @param array $data - данные текущей строки - * @return mixed + * @inheritdoc */ - public function genListHTML(&$row, $data) + public function generateRow(&$row, $data) { $text = $this->getValue(); if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { $row->AddInputField($this->getCode(), array('style' => 'width:90%')); - } else { if (strlen($text) > self::LIST_TEXT_SIZE && !$this->isExcelView()) { $pos = false; @@ -64,9 +60,9 @@ public function genListHTML(&$row, $data) $text = substr($text, 0, $pos) . " ..."; } + $text = static::prepareToOutput($text); + $row->AddViewField($this->code, $text); } - } - } \ No newline at end of file diff --git a/lib/widget/UrlWidget.php b/lib/widget/UrlWidget.php new file mode 100644 index 0000000..3bc1b55 --- /dev/null +++ b/lib/widget/UrlWidget.php @@ -0,0 +1,85 @@ + + *
  • PROTOCOL_REQUIRED - ссылка должна иметь протокол
  • + *
  • STYLE - inline-стили
  • + *
  • SIZE - значение атрибута size для input
  • + *
  • MAX_URL_LEN - длина отображаемого URL
  • + * + * + * @author Nik Samokhvalov + */ +class UrlWidget extends StringWidget +{ + static protected $defaults = array( + 'MAX_URL_LEN' => 256, + 'PROTOCOL_REQUIRED' => false, + ); + + /** + * @inheritdoc + */ + public function generateRow(&$row, $data) + { + $value = $this->getValue(); + + if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) { + $row->AddInputField($this->getCode(), array('style' => 'width:90%')); + } + + $row->AddViewField($this->getCode(), $value); + } + + /** + * @inheritdoc + */ + public function getValue() + { + $code = $this->getCode(); + $value = isset($this->data[$code]) ? $this->data[$code] : null; + + if ($value !== null) { + $urlText = static::prepareToOutput($value); + $urlText = preg_replace('/^javascript:/i', '', $urlText); + + if (strlen($urlText) > $this->getSettings('MAX_URL_LEN')) { + $urlText = substr($urlText, 0, $this->getSettings('MAX_URL_LEN')); + } + + if (($this->getSettings('READONLY') && $this->getCurrentViewType() == static::EDIT_HELPER) || $this->getCurrentViewType() == static::LIST_HELPER) { + $value = '' . $urlText . ''; + } else { + $value = $urlText; + } + } + + return $value; + } + + /** + * @inheritdoc + */ + public function processEditAction() + { + $value = $this->getValue(); + + if ( + $this->getSettings('PROTOCOL_REQUIRED') + && !empty($value) + && preg_match('/^https?:\/\//', $value) == 0 + ) { + + $this->addError('PROTOCOL_REQUIRED'); + } + } +} \ No newline at end of file diff --git a/lib/widget/UserWidget.php b/lib/widget/UserWidget.php new file mode 100644 index 0000000..0aede51 --- /dev/null +++ b/lib/widget/UserWidget.php @@ -0,0 +1,100 @@ + + *
  • STYLE - inline-стили + *
  • SIZE - значение атрибута size для input + * + * + * @author Nik Samokhvalov + */ +class UserWidget extends NumberWidget +{ + /** + * @inheritdoc + */ + public function getEditHtml() + { + $style = $this->getSettings('STYLE'); + $size = $this->getSettings('SIZE'); + + $userId = $this->getValue(); + + $htmlUser = ''; + + if (!empty($userId) && $userId != 0) { + $rsUser = UserTable::getById($userId); + $user = $rsUser->fetch(); + + $htmlUser = '[' . $user['ID'] . '] (' + . $user['EMAIL'] . ') ' . $user['NAME'] . ' ' . $user['LAST_NAME']; + } + + return '' . $htmlUser; + } + + /** + * @inheritdoc + */ + public function getValueReadonly() + { + $userId = $this->getValue(); + $htmlUser = ''; + + if (!empty($userId) && $userId != 0) { + $rsUser = UserTable::getById($userId); + $user = $rsUser->fetch(); + + $htmlUser = '[' . $user['ID'] . ']'; + + if ($user['EMAIL']) { + $htmlUser .= ' (' . $user['EMAIL'] . ')'; + } + + $htmlUser .= ' ' . static::prepareToOutput($user['NAME']) + . ' ' . static::prepareToOutput($user['LAST_NAME']); + } + + return $htmlUser; + } + + /** + * @inheritdoc + */ + public function generateRow(&$row, $data) + { + $userId = $this->getValue(); + $strUser = ''; + + if (!empty($userId) && $userId != 0) { + $rsUser = UserTable::getById($userId); + $user = $rsUser->fetch(); + + $strUser = '[' . $user['ID'] . ']'; + + if ($user['EMAIL']) { + $strUser .= ' (' . $user['EMAIL'] . ')'; + } + + $strUser .= ' ' . static::prepareToOutput($user['NAME']) + . ' ' . static::prepareToOutput($user['LAST_NAME']); + } + + if ($strUser) { + $row->AddViewField($this->getCode(), $strUser); + } else { + $row->AddViewField($this->getCode(), ''); + } + } +} \ No newline at end of file diff --git a/lib/widget/VisualEditorWidget.php b/lib/widget/VisualEditorWidget.php index 9ff9718..112b70c 100644 --- a/lib/widget/VisualEditorWidget.php +++ b/lib/widget/VisualEditorWidget.php @@ -2,120 +2,251 @@ namespace DigitalWand\AdminHelper\Widget; -use Bitrix\Main\Loader; - class VisualEditorWidget extends TextAreaWidget { - static protected $defaults = array( + protected static $defaults = array( 'WIDTH' => '100%', 'HEIGHT' => 450, 'EDITORS' => array( 'EDITOR' ), 'DEFAULT_EDITOR' => 'EDITOR', + 'LIGHT_EDITOR_MODE' => 'N', + 'EDITOR_TOOLBAR_CONFIG_SET' => 'FULL', // SIMPLE + 'EDITOR_TOOLBAR_CONFIG' => false, ); - protected function genEditHTML() + /** + * @inheritdoc + */ + protected function getEditHtml() { - if (Loader::IncludeModule("fileman")) { - \CJSCore::Init(array('jquery')); + if (\CModule::IncludeModule("fileman")) { ob_start(); + $codeType = $this->code . '_TEXT_TYPE'; + /** @var string $className Имя класса без неймспейса */ + $className = $this->getEntityShortName(); + $entityClass = $this->entityName; + $modelPk = $entityClass::getEntity()->getPrimary(); + $id = isset($this->data[$modelPk]) ? $this->data[$modelPk] : false; + $bxCode = $this->code . '_' . $className; + $bxCodeType = $codeType . '_' . $className; + + if ($this->forceMultiple) { + if ($id) { + $bxCode .= '_' . $id; + $bxCodeType .= '_' . $id; + } else { + $bxCode .= '_new_'; + $bxCodeType .= '_new_'; + } + } - $codeType = $this->getCode() . '_TEXT_TYPE'; + // TODO Избавиться от данного костыля + if ($_REQUEST[$bxCode]) { + $this->data[$this->code] = $_REQUEST[$bxCode]; + } - \CFileMan::AddHTMLEditorFrame( - $this->getCode(), - $this->getValue(), - $codeType, - $this->data[$codeType], - array( - 'width' => $this->getSettings('WIDTH'), - 'height' => $this->getSettings('HEIGHT'), + $editorToolbarSets = array( + 'FULL' => array( + 'Bold', 'Italic', 'Underline', 'Strike', 'RemoveFormat', + 'CreateLink', 'DeleteLink', 'Image', 'Video', + 'BackColor', 'ForeColor', + 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyFull', + 'InsertOrderedList', 'InsertUnorderedList', 'Outdent', 'Indent', + 'StyleList', 'HeaderList', + 'FontList', 'FontSizeList' + ), + 'SIMPLE' => array( + 'Bold', 'Italic', 'Underline', 'Strike', 'RemoveFormat', + 'CreateLink', 'DeleteLink', + 'Video', + 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyFull', + 'InsertOrderedList', 'InsertUnorderedList', 'Outdent', 'Indent', + 'FontList', 'FontSizeList', ) ); - $defaultEditors = array("text" => "text", "html" => "html", "editor" => "editor"); - $editors = $this->getSettings('EDITORS'); - $defaultEditor = strtolower($this->getSettings('DEFAULT_EDITOR')); - - $contentType = $this->data[$codeType]; - $defaultEditor = isset($contentType) && $contentType == "text" ? "text" : $defaultEditor; - $defaultEditor = isset($contentType) && $contentType == "html" ? "editor" : $defaultEditor; - - if (count($editors) > 1) { - foreach ($editors as &$editor) { - $editor = strtolower($editor); - if (isset($defaultEditors[$editor])) { - unset($defaultEditors[$editor]); + if ($this->getSettings('LIGHT_EDITOR_MODE') == 'Y') { + /** + * Облегченная версия редактора + */ + global $APPLICATION; + $editorToolbarConfig = $this->getSettings('EDITOR_TOOLBAR_CONFIG'); + if (!is_array($editorToolbarConfig)) { + $editorToolbarSet = $this->getSettings('EDITOR_TOOLBAR_CONFIG_SET'); + if (isset($editorToolbarSets[$editorToolbarSet])) { + $editorToolbarConfig = $editorToolbarSets[$editorToolbarSet]; + } else { + $editorToolbarConfig = $editorToolbarSets['FULL']; + } + } + $APPLICATION->IncludeComponent("bitrix:fileman.light_editor", "", array( + "CONTENT" => $this->data[$this->code], + "INPUT_NAME" => $bxCode, + "INPUT_ID" => $bxCode, + "WIDTH" => $this->getSettings('WIDTH'), + "HEIGHT" => $this->getSettings('HEIGHT'), + "RESIZABLE" => "N", + "AUTO_RESIZE" => "N", + "VIDEO_ALLOW_VIDEO" => "Y", + "VIDEO_MAX_WIDTH" => $this->getSettings('WIDTH'), + "VIDEO_MAX_HEIGHT" => $this->getSettings('HEIGHT'), + "VIDEO_BUFFER" => "20", + "VIDEO_LOGO" => "", + "VIDEO_WMODE" => "transparent", + "VIDEO_WINDOWLESS" => "Y", + "VIDEO_SKIN" => "/bitrix/components/bitrix/player/mediaplayer/skins/bitrix.swf", + "USE_FILE_DIALOGS" => "Y", + "ID" => 'LIGHT_EDITOR_' . $bxCode, + "JS_OBJ_NAME" => $bxCode, + 'TOOLBAR_CONFIG' => $editorToolbarConfig + ) + ); + } else { + /** + * Полная версия редактора + */ + \CFileMan::AddHTMLEditorFrame( + $bxCode, + $this->data[$this->code], + $bxCodeType, + $this->data[$codeType], + array( + 'width' => $this->getSettings('WIDTH'), + 'height' => $this->getSettings('HEIGHT'), + ) + ); + $defaultEditors = array("text" => "text", "html" => "html", "editor" => "editor"); + $editors = $this->getSettings('EDITORS'); + $defaultEditor = strtolower($this->getSettings('DEFAULT_EDITOR')); + $contentType = $this->data[$codeType]; + $defaultEditor = isset($contentType) && $contentType == "text" ? "text" : $defaultEditor; + $defaultEditor = isset($contentType) && $contentType == "html" ? "editor" : $defaultEditor; + + if (count($editors) > 1) { + foreach ($editors as &$editor) { + $editor = strtolower($editor); + if (isset($defaultEditors[$editor])) { + unset($defaultEditors[$editor]); + } } } - } - - - $script = ''; + foreach ($defaultEditors as $editor) { + $script .= '$("#bxed_' . $bxCode . '_' . $editor . '").parent().hide();'; + } - echo $script; + $script .= '$("#bxed_' . $bxCode . '_' . $defaultEditor . '").click();'; + $script .= 'setTimeout(function() {$("#bxed_' . $bxCode . '_' . $defaultEditor . '").click(); }, 500);'; + $script .= "});"; + $script .= ''; + echo $script; + } $html = ob_get_clean(); - return $html; + return $html; } else { - return parent::genEditHTML(); + return parent::getEditHtml(); } } - public function genBasicEditField($isPKField) + /** + * @inheritdoc + */ + public function showBasicEditField($isPKField) { - if (!Loader::IncludeModule("fileman")) { - parent::genBasicEditField($isPKField); - + if (!\CModule::IncludeModule("fileman")) { + parent::showBasicEditField($isPKField); } else { $title = $this->getSettings('TITLE'); if ($this->getSettings('REQUIRED') === true) { $title = '' . $title . ''; } - print '' . $title . ''; print ''; $readOnly = $this->getSettings('READONLY'); if (!$readOnly) { - print $this->genEditHTML(); + print $this->getEditHtml(); } else { print $this->getValueReadonly(); } - print ''; } } + /** + * @inheritdoc + */ public function processEditAction() { + $entityClass = $this->entityName; + $modelPk = $entityClass::getEntity()->getPrimary(); + $className = $this->getEntityShortName(); $currentView = $this->getCurrentViewType(); + switch ($currentView) { case HelperWidget::EDIT_HELPER: - + $id = isset($this->data[$modelPk]) ? $this->data[$modelPk] : false; $codeType = $this->getCode() . '_TEXT_TYPE'; - - $this->setValue($_REQUEST[$this->getCode()]); - $this->data[$codeType] = $_REQUEST[$codeType]; - + $bxCode = $this->getCode() . '_' . $className; + $bxCodeType = $codeType . '_' . $className; + if ($this->forceMultiple AND $id) { + $bxCode .= '_' . $id; + $bxCodeType .= '_' . $id; + } + if (!$_REQUEST[$bxCode] && $this->getSettings('REQUIRED') == true) { + $this->addError('REQUIRED_FIELD_ERROR'); + } + $this->data[$this->code] = $_REQUEST[$bxCode]; + $this->data[$codeType] = $_REQUEST[$bxCodeType]; break; - case HelperWidget::LIST_HELPER: default: parent::processEditAction(); break; + } + } + /** + * @inheritdoc + */ + protected function getValueReadonly() + { + return static::prepareToOutput($this->data[$this->code]); + } + + /** + * @inheritdoc + */ + public function generateRow(&$row, $data) + { + $text = trim(strip_tags($data[$this->code])); + + if (strlen($text) > self::LIST_TEXT_SIZE && !$this->isExcelView()) { + $pos = false; + $pos = $pos === false ? stripos($text, " ", self::LIST_TEXT_SIZE) : $pos; + $pos = $pos === false ? stripos($text, "\n", self::LIST_TEXT_SIZE) : $pos; + $pos = $pos === false ? stripos($text, "AddViewField($this->code, $text); + } + + /** + * Название класса без неймспейса. + * + * @return string + */ + protected function getEntityShortName() + { + return end(explode('\\', $this->entityName)); } } \ No newline at end of file