<?php
/**
 * Класс для работы с гридам (таблицами данных)
 */
class cmsGrid {

    /**
     * Параметры грида
     *
     * @var array
     */
    private $grid = [
        // URL, откуда загружать данные
        'source_url' => '',
        // Опции списка
        'options' => [
            'order_by'          => 'id', // Поле сортировки
            'order_to'          => 'asc', // Направление сортировки
            'show_id'           => true, // Показывать id записи
            'is_sortable'       => true, // Можно сортировать
            'is_filter'         => true, // Поля грида можно фильтровать
            'advanced_filter'   => false, // URL для дополнительного фильтра записей
            'is_pagination'     => true, // показывать пагинацию
            'perpage'           => 30, // записей на странице по умолчанию
            'is_toolbar'        => true, // выводить тулбар
            'is_draggable'      => false, // строки могут меняться местами мышью
            'drag_save_url'     => '', // URL для сохранения при драг-эн-дроп
            'save_action'       => '', // URL для сохранения при инлайн редактировании
            'is_selectable'     => false, // Строки могут выделяться
            'deny_mass_editing' => false, // Запретить массовое редактирование списка, если включено editable
            /**
             * select_actions - это массив действий над выделенными строками
             * Пример в system/controllers/admin/grids/grid_content_items.php
              'title'   => Название,
              'action'  => submit || open,
              'confirm' => Фраза подтверждения действия,
              'url'     => URL на который отправлять выделенные записи
             */
            'select_actions'    => false
        ],
        // Колонки параметров
        'columns' => [],
        // Действия над записью
        'actions' => [],
        // Фильтр записей. Не заполняйте его из функции описания грида
        'filter'  => []
    ];

    /**
     * Все значения колонки по умолчанию
     * @var array
     */
    private $default_column = [
        'tooltip'           => '',  // Текст всплывающей подсказки
        'tooltip_handler'   => null,  // Функция обработчик, возвращающая текст всплывающей подсказки
        'switchable'        => false, // Может включаться/выключаться
        'disable'           => false, // Если может включаться/выключаться выключение по умолчанию
        'sortable'          => true,  // Можено сортировать по ней
        'title'             => '',    // Заголовок
        'class'             => '',    // CSS класс
        'class_handler'     => null,  // Функция обработчик значения для CSS класса
        'key_alias'         => '',    // Псевдоним поля
        'handler'           => null,  // Функция обработчик значения колонки
        'flag'              => false, // Колонка - флаг (включена/выключена)
        'flag_on'           => null,  // Значение, с которым сравнивается запись, чтобы флажок был включен
        'flag_handler'      => null,  // Функция обработчик значения для флага
        'flag_toggle'       => '',    // URL переключения состояния флага
        'href'              => '',    // Ссылка колонки
        'href_handler'      => null,  // Функция обработчик, если возвращает true, то ссылка показывается
        'order_by'          => '',    // Имя поля, по которому нужно сортировать
        'filter_by'         => '',    // Имя поля, по которому нужно фильтровать
        'filter'            => '',    // Тип фильтра, см. applyGridFilter
        'filter_select'     => ['items' => []], // Массив значений для select тега фильтра
        'filter_attributes' => [],    // Атрибуты тега фильтрации
        'editable' => [               // Может редактироваться из списка
            'rules'            => [],      // Массив правил валидации при сохранении
            'renderer'         => null,    // Компонент vue поля редактирования, по умолчанию form-input
            'items'            => null,    // Массив списка (или функция возвращающая массив) для селекта, если renderer form-select
            'language_context' => false,   // Если выключено, будет искать языковое поле для текущей локали
            'id_field'         => 'id',    // Имя поля записи, по которому искать уникальное значение
            'table'            => null,    // Таблица для сохранения
            'save_action'      => '',      // URL для сохранения
            'attributes'       => []       // Атрибуты тега быстрого редактирования
        ]
    ];

    /**
     * Колонки, которые нужно показывать
     *
     * @var ?array
     */
    private $visible_columns = null;

    /**
     * Имена колонок ключи, включена/выключена значение
     *
     * @var array
     */
    private $visible_columns_names = [];

    /**
     * Колонки, которые можно отключать
     *
     * @var ?array|boolean
     */
    private $switchable_columns = null;

    /**
     * Маппинг имя фильтра => компонент (vuejs) отрисовки
     * @var array
     */
    private $filter_component_map = [
        'range_date' => 'form-date-range',
        'range'      => 'form-range',
        'zero'       => 'form-checkbox',
        'nn'         => 'form-checkbox',
        'ni'         => 'form-checkbox',
        'in'         => 'form-multiselect',
        'filled'     => 'form-select',
        'exact'      => 'form-select',
        'like'       => 'form-input',
        'ip'         => 'form-input',
        'date'       => 'form-date'
    ];

    /**
     * Объект контроллера
     * @var cmsController
     */
    private $controller;

    /**
     * Имя грида
     * @var string
     */
    private $grid_name = '';

    /**
     * Параметры, передающиеся в функцию грида
     * @var array
     */
    private $grid_params = [];

    /**
     * Грид успешно загружен?
     * @var boolean
     */
    private $is_loaded = false;

    /**
     * В гриде есть столбцы с инлайн редактированием
     * @var array
     */
    private $editable = false;

    /**
     * URI для режима редактирования
     * @var string
     */
    private $edit_url = '';

    /**
     * Текст ошибки при инициализации
     * @var ?string
     */
    private $error = null;

    /**
     *
     * @param cmsController|cmsAction $controller Объект контроллера (cmsAction с магией тоже), для которого строим грид
     * @param string $grid_name Имя грида
     * @param ?array $grid_params Параметры инициализации
     */
    public function __construct($controller, $grid_name, $grid_params = null) {

        $this->controller = $controller;

        $this->grid_name = $grid_name;

        if($grid_params) {

            $this->grid_params = !is_array($grid_params) ? [$grid_params] : $grid_params;
        }

        $this->is_loaded = $this->load();
    }

    public function &__get($name) {
        return $this->grid[$name];
    }

    public function __set($name, $value) {
        $this->grid[$name] = $value;
    }

    public function __isset($name) {
        return isset($this->grid[$name]);
    }

    public function __unset($name) {
        unset($this->grid[$name]);
    }

    /**
     * Грид успешно загружен?
     *
     * @return boolean
     */
    public function isLoaded() {
        return $this->is_loaded;
    }

    /**
     * Возвращает последнюю ошибку
     *
     * @return ?string
     */
    public function getError() {
        return $this->error;
    }

    /**
     * Загружает и подготовливает параметры грида
     *
     * @return bool
     */
    private function load() {

        $grid_file = $this->controller->root_path . 'grids/grid_' . $this->grid_name . '.php';

        if (!is_readable($grid_file)) {

            $this->error = ERR_FILE_NOT_FOUND . ': '. str_replace(PATH, '', $grid_file);

            return false;
        }

        include_once $grid_file;

        $grid_func_name = 'grid_' . $this->grid_name;

        if(!function_exists($grid_func_name)){

            $this->error = $grid_func_name.' function not found in '. str_replace(PATH, '', $grid_file);

            return false;
        }

        $args = [$this->controller];

        foreach ($this->grid_params as $p) {
            $args[] = $p;
        }

        $grid = call_user_func_array($grid_func_name, $args);

        foreach ($grid as $key => $data) {

            $this->grid[$key] = is_array($data) ? array_merge(($this->grid[$key] ?? []), $data) : $data;
        }

        // Фильтр по умолчанию
        $this->setDefaultFilter();

        $this->grid = cmsEventsManager::hook('grid_' . $this->getGridFullName(), $this->grid);

        if($this->grid['options']['select_actions']){
            array_unshift($this->grid['options']['select_actions'], [
                'title' => LANG_SELECTED_ACTIONS,
                'url'   => ''
            ]);
        }

        list($this->grid, $args) = cmsEventsManager::hook(
            'grid_' . $this->getGridFullName() . '_args',
            [$this->grid, $args]
        );

        return true;
    }

    /**
     * Возвращает массив грида целиком
     * @return array
     */
    public function getGrid() {
        return $this->grid;
    }

    /**
     * Возвращает значение описания грида
     *
     * @param string $path Путь до ключа, например columns:name:title
     * @return mixed
     */
    public function getGridValue($path) {
        return array_value_recursive($path, $this->grid);
    }

    /**
     * Возвращает имя грида с учётом контроллера
     *
     * @return string
     */
    public function getGridFullName() {
        return $this->controller->name . '_' . $this->grid_name;
    }

    /**
     * Возвращает имя грида
     *
     * @return string
     */
    public function getGridName() {
        return $this->grid_name;
    }

    /**
     * Валидация значения колонки
     * аналогично как в формах
     *
     * @param string $field_name
     * @param mixed $value
     * @return boolean
     */
    public function validateColumnValue($field_name, $value) {

        $rules = $this->grid['columns'][$field_name]['editable']['rules'] ?? [];

        if(!$rules){
            return true;
        }

        foreach ($rules as $rule) {

            if (!$rule) { continue; }

            $validate_function = "validate_{$rule[0]}";

            $rule[] = $value;

            unset($rule[0]);

            $result = call_user_func_array([$this->controller, $validate_function], $rule);

            // ошибка уже найдена
            if ($result !== true) {
                return $result;
            }
        }

        return true;
    }

    /**
     * Подготоваливает видимые колонки
     */
    private function getVisibleColumns() {

        if ($this->visible_columns === null) {

            foreach ($this->grid['columns'] as $name => $column) {

                $is_disabled = false;

                if (!empty($column['disable'])) {
                    $is_disabled = true;
                }
                if (array_key_exists($name, $this->visible_columns_names)) {
                    $is_disabled = !$this->visible_columns_names[$name];
                }
                if(empty($column['switchable'])){
                    $is_disabled = false;
                }
                if ($is_disabled) {
                    continue;
                }

                $this->visible_columns[$name] = $column;
            }
        }

        return $this->visible_columns;
    }

    /**
     * Отключаемые колонки
     *
     * @return array
     */
    public function getSwitchableColumns() {

        if ($this->switchable_columns === null) {

            $columns = [];

            foreach ($this->grid['columns'] as $key => $item) {
                if (!empty($item['switchable'])) {
                    $columns[$key] = $item['title'];
                }
            }

            $this->switchable_columns = $columns ?: false;
        }

        return $this->switchable_columns;
    }

    /**
     * Отключает колонку для вывода
     *
     * @param string $name Имя колонки
     * @return void
     */
    public function disableColumn($name) {
        $this->visible_columns_names[$name] = false;
    }

    /**
     * Включает колонку для вывода
     *
     * @param string $name Имя колонки
     * @return void
     */
    public function enableColumn($name) {
        $this->visible_columns_names[$name] = true;
    }

    /**
     * Устанавливает параметры выборки из БД по умолчанию
     *
     * @return void
     */
    private function setDefaultFilter() {

        $this->grid['filter'] = [
            'page' => 1,
            'columns' => [],
            'advanced_filter' => '',
            'perpage' => $this->grid['options']['perpage']
        ];

        if ($this->grid['options']['order_by']) {
            $this->grid['filter']['order_by'] = $this->grid['options']['order_by'];
        }

        if ($this->grid['options']['order_to']) {
            $this->grid['filter']['order_to'] = $this->grid['options']['order_to'];
        }
    }

    /**
     * Добавляет данные в фильтр
     *
     * @param array $filter
     * @return void
     */
    public function addToFilter($filter) {
        $this->grid['filter'] = array_replace_recursive($this->grid['filter'], $filter);
    }

    /**
     * Применяет фильтр к модели выборки
     *
     * @param cmsModel $model Объект модели, где выбираем записи
     * @param array $filter Массив фильтрации
     * @param array $table_name Таблица, где ищем поля фильтра
     * @return cmsModel
     */
    public function applyGridFilter(cmsModel $model, $filter, $table_name) {

        // применяем сортировку
        if (!empty($filter['order_by']) && !empty($filter['order_to'])) {

            // Есть ли вообще такой столбец
            if(empty($this->grid['columns'][$filter['order_by']])){
                $filter['order_by'] = $this->grid['options']['order_by'];
            }

            $order_by = $filter['order_by'];

            // Есть отдельный столбец для сортировки
            if (!empty($this->grid['columns'][$order_by]['order_by'])) {
                $order_by = $this->grid['columns'][$order_by]['order_by'];
            }

            $model->orderBy($order_by, $filter['order_to']);
        }

        // устанавливаем страницу
        if (!empty($filter['page'])) {

            $filter['perpage'] = !empty($filter['perpage']) ? (int) $filter['perpage'] : 30;
            $filter['page']    = (int) ($filter['page'] <= 0 ? 1 : $filter['page']);

            $model->limitPage($filter['page'], $filter['perpage']);
        }

        // Пагинация отключена
        if(!$this->grid['options']['is_pagination']){
            $model->limit(false);
        }

        //
        // проходим по каждой колонке таблицы
        // и проверяем не передан ли фильтр для нее
        //
        foreach ($this->getVisibleColumns() as $field => $column) {

            if (empty($column['filter']) ||
                    $column['filter'] === 'none' ||
                    !array_key_exists($field, $filter) ||
                    is_empty_value($filter[$field])) {
                continue;
            }

            if (!empty($column['filter_by'])) {
                $filter_field = $column['filter_by'];
            } else {
                $filter_field = $field;
            }

            $value = $filter[$field];

            switch ($column['filter']) {
                case 'range_date':
                    if (isset($value['from']) && !is_empty_value($value['from'])) {
                        $date_from = date('Y-m-d', strtotime($value['from']));
                        $model->filterGtEqual($filter_field, $date_from);
                    }
                    if (isset($value['to']) && !is_empty_value($value['to'])) {
                        $date_to = date('Y-m-d', strtotime($value['to']));
                        $model->filterLtEqual($filter_field, $date_to);
                    }
                case 'range':
                    if (isset($value['from']) && !is_empty_value($value['from'])) {
                        $model->filterGtEqual($filter_field, $value['from']);
                    }
                    if (isset($value['to']) && !is_empty_value($value['to'])) {
                        $model->filterLtEqual($filter_field, $value['to']);
                    }
                    break;
                case 'zero':
                    if($value) {
                        $model->filterEqual($filter_field, 0);
                    }
                case 'nn':
                    if($value) {
                        $model->filterNotNull($filter_field);
                    }
                    break;
                case 'ni':
                    if($value) {
                        $model->filterIsNull($filter_field);
                    }
                    break;
                case 'in': $model->filterIn($filter_field, !is_array($value) ? explode(',', $value) : $value);
                    break;
                case 'filled': ($value ? $model->filterNotNull($filter_field) : $model->filterIsNull($filter_field));
                    break;
                case 'exact': $model->filterEqual($filter_field, $value);
                    break;
                case 'ip': $model->filterEqual($filter_field, string_iptobin($value), true);
                    break;
                case 'like': $model->filterLike($filter_field, "%{$value}%");
                    break;
                case 'date':
                    $date = date('Y-m-d', strtotime($value));
                    $model->filterLike($filter_field, "%{$date}%");
                    break;
                case 'callback':
                    if (empty($column['filter_callback'])) {
                        $model->filter('1 = 0');
                    } else if (is_callable($column['filter_callback'])) {
                        call_user_func_array($column['filter_callback'], [$filter_field, $value, $model]);
                    }
                    break;
            }
        }

        // Запоминаем
        $this->grid['filter'] = array_merge($this->grid['filter'], $filter);

        // Дополнительный фильтр
        if (!empty($filter['advanced_filter']) && is_string($filter['advanced_filter'])) {

            $dataset_filters = [];

            parse_str($filter['advanced_filter'], $dataset_filters);

            if (!$model->applyDatasetFilters($dataset_filters, true, [], $table_name)) {

                $this->grid['filter']['advanced_filter'] = '';
            }
        }

        return $model;
    }

    /**
     * Собирает все данные таблицы
     *
     * @param array|false $dataset Данные из базы
     * @param integer $total Сколько всего записей
     * @return array
     */
    public function makeGridRows($dataset = false, $total = 0) {

        $rows = [];

        if(is_array($dataset)){
            foreach($dataset as $row){
                $rows[] = [
                    'columns'  => $this->makeRowColumns($row),
                    'id'       => $row['id'],
                    'selected' => false,
                    'edited'   => false
                ];
            }
        }

        return [
            'dragging'   => -1,
            'is_loading' => false,
            'need_load'  => $dataset === false,
            'source_url' => $this->grid['source_url'] !== '' ? $this->grid['source_url'] : cmsCore::getInstance()->uri_absolute,
            'edit_url'   => $this->edit_url,
            'options'    => $this->grid['options'],
            'filter'     => $this->grid['filter'],
            'rows'       => $rows,
            'total'      => $total ? $total : count(($dataset ?: [])),
            'editable'   => $this->editable,
            'switchable' => [
                'title'   => LANG_GRID_COLYMNS_SETTINGS,
                'columns' => $this->getSwitchableColumns()
            ],
            'columns'    => $this->makeColumns()
        ];
    }

    /**
     * Собирает строку таблицы
     *
     * @param array $row
     * @return array
     */
    private function makeRowColumns($row) {

        $columns = [];

        $is_set_dragged_class = false;

        foreach ($this->getVisibleColumns() as $field => $column) {

            if (isset($column['key_alias'])){
                $field = $column['key_alias'];
            }

            $class = [];

            if (!empty($column['class'])) {
                $class[] = $column['class'];
            }

            if (isset($column['class_handler'])) {
                $class[] = $column['class_handler']($row);
            }

            if ($field === 'id') {
                if (!$this->grid['options']['show_id']) {

                    $class[] = 'd-none';
                } else {

                    $class[] = 'dragged_handle';

                    $is_set_dragged_class = true;
                }
            } else if (!$is_set_dragged_class) {

                $class[] = 'dragged_handle';

                $is_set_dragged_class = true;
            }

            $row_column = [
                'row_id'   => $row['id'],
                'tooltip'  => $column['tooltip'] ?? '',
                'name'     => $field,
                'value'    => '',
                'class'    => implode(' ', $class),
                'editable' => $this->getEditableParams($row, $column, $field),
                'href'     => false,
                'renderer' => $column['renderer'] ?? 'basic' // basic, flag, html, actions
            ];

            // Это скорее чтобы не было ошибки при рендере
            if (!array_key_exists($field, $row)) {

                $columns[] = $row_column;

                continue;
            }

            // тултип колонки
            if (isset($column['tooltip_handler'])) {
                $row_column['tooltip'] = $column['tooltip_handler']($row);
            }

            // null - значит будет пустая строка
            $row_column['value'] = $row[$field] ?? '';

            if (isset($column['href_handler'])) {
                $is_active_href = $column['href_handler']($row);
            } else {
                $is_active_href = true;
            }

            // если из значения нужно сделать ссылку, то парсим шаблон
            // адреса, заменяя значения полей
            if (isset($column['href'])) {
                if ($is_active_href) {
                    $row_column['href'] = string_replace_keys_values_extended($column['href'], $row);
                }
            }

            // Тип флаг
            if (!empty($column['flag'])) {

                if (isset($column['flag_handler'])) {

                    $row_column['value'] = $column['flag_handler']($row_column['value'], $row);
                }

                // Есть предустановленное значение для "включенности" флага
                if (isset($column['flag_on'])) {

                    $row_column['value'] = $row_column['value'] == $column['flag_on'] ? 1 : 0;
                } else {

                    $row_column['value'] = (int) $row_column['value'];
                }

                // URL для смены состояния флага
                $flag_toggle_url = $column['flag_toggle'] ?? false;
                if ($flag_toggle_url) {
                    $flag_toggle_url = string_replace_keys_values($flag_toggle_url, $row);
                }

                $row_column['flag_class'] = $column['flag'] === true ? 'flag' : $column['flag'];
                $row_column['href']       = $is_active_href ? $flag_toggle_url : false;
                $row_column['confirm']    = $column['flag_confirm'] ?? false;
                $row_column['renderer']   = 'flag';

                $columns[] = $row_column;

                continue;
            }

            // Есть функция обработчик, предполагаем, что вернёт готовый HTML
            if (isset($column['handler'])) {

                $row_column['renderer'] = 'html';
                $row_column['value']    = $column['handler']($row_column['value'], $row);

                $columns[] = $row_column;

                continue;
            }

            // Массивы отдаём на откуп разработчикам
            if (is_array($row_column['value'])) {

                if (isset($column['renderer'])) {

                    // Можно в колонке указать компонент отрисовки (vue) колонки
                    $row_column['renderer'] = $column['renderer'];

                } else {
                    $row_column['value'] = '!error grid value!';
                }
            }

            $columns[] = $row_column;
        }

        // если есть колонка действий, то формируем набор ссылок
        if ($this->grid['actions']) {

            $row_column = [
                'row_id'   => $row['id'],
                'value'    => [],
                'editable' => false,
                'class'    => false,
                'href'     => false,
                'renderer' => 'actions'
            ];

            foreach ($this->grid['actions'] as $action) {

                if (isset($action['handler'])) {

                    $is_active = $action['handler']($row);

                    unset($action['handler']);
                } else {
                    $is_active = true;
                }

                if (!$is_active) {
                    continue;
                }

                // парсим шаблон если есть, заменяя значения полей
                foreach ($action as $akey => $aval) {
                    if (is_string($aval)) {
                        $action[$akey] = string_replace_keys_values_extended($aval, $row);
                    }
                }

                // все действия с подтверждением снабжаем csrf_token
                if (isset($action['confirm']) && !empty($action['href'])) {
                    $action['href'] .= (strpos($action['href'], '?') !== false ? '&' : '?') . 'csrf_token=' . cmsForm::getCSRFToken();
                }

                if (!empty($action['icon'])) {

                    $icon_params = explode(':', $action['icon']);

                    if (!isset($icon_params[1])) {
                        array_unshift($icon_params, 'solid');
                    }

                    $action['icon'] = html_svg_icon($icon_params[0], $icon_params[1], 16, false);
                }

                $row_column['value'][] = $action;
            }

            $columns[] = $row_column;
        }

        return $columns;
    }

    /**
     * Собирает заголовки таблицы
     *
     * @return array
     */
    private function makeColumns() {

        $columns = [];

        foreach ($this->getVisibleColumns() as $name => $column) {

            $column = array_merge($this->default_column, $column);

            $class = [];

            if (!empty($column['class'])) {
                $class[] = $column['class'];
            }

            if ($name === 'id' && !$this->grid['options']['show_id']) {
                $class[] = 'd-none';
            }

            $filter = false;

            if($column['filter'] && $column['filter'] !== 'none'){

                $filter = [
                    'component' => $this->filter_component_map[$column['filter']],
                    'params'    => [
                        'attributes' => array_merge($column['filter_attributes'], [
                            'name' => $name
                        ])
                    ]
                ];

                switch ($filter['component']) {
                    case 'form-date-range':
                    case 'form-range':
                        $filter['params']['lang_from'] = LANG_FROM;
                        $filter['params']['lang_to'] = LANG_TO;
                        break;
                    case 'form-input':
                        $filter['params']['attributes']['type'] = 'search';
                        break;
                    case 'form-checkbox':
                        $filter['params']['title'] = $column['filter_checkbox'] ?? false;
                        break;
                    case 'form-multiselect':
                    case 'form-select':
                        $filter['params']['items'] = is_array($column['filter_select']['items']) ? $column['filter_select']['items'] : $column['filter_select']['items']($name, $this->grid['filter']);
                        if(!$filter['params']['items']){
                            $filter['component'] = 'form-input';
                            $filter['params']['attributes']['type'] = 'search';
                        }
                        break;
                }
            }

            $columns[] = [
                'width'    => $column['width'] ?? '',
                'title'    => $column['title'] ?? '',
                'name'     => $name,
                'filter'   => $filter,
                'class'    => implode(' ', $class),
                'sortable' => $this->grid['options']['is_sortable'] && $column['sortable'],
            ];
        }

        if ($this->grid['actions']) {

            $filter = [
                'component' => 'form-filter',
                'params' => [
                    'href'        => $this->grid['options']['advanced_filter'],
                    'lang_filter' => LANG_FILTER,
                    'lang_cancel' => LANG_CANCEL,
                    'icon_filter' => html_svg_icon('solid', 'search-plus', 16, false),
                    'icon_cancel' => html_svg_icon('solid', 'search-minus', 16, false)
                ]
            ];

            $columns[] = [
                'sortable' => false,
                'width'    => count($this->grid['actions']) * 30,
                'title'    => LANG_CP_ACTIONS,
                'class'    => 'text-right align-middle',
                'name'     => 'advanced_filter',
                'filter'   => $this->grid['options']['advanced_filter'] ? $filter : false
            ];
        }

        return $columns;
    }

    private function getEditableParams($row, $column, $field) {

        $id_field = $column['editable']['id_field'] ?? 'id';

        if (!array_key_exists('editable', $column) || empty($row[$id_field])) {
            return false;
        }

        $save_action_query = ['csrf_token' => cmsForm::getCSRFToken()];

        // Экшен списка записей должен реализовывать и сохранение поля столбца
        $save_action = $this->grid['source_url'];

        // Может быть указан для грида другой урл
        if (!empty($this->grid['options']['save_action'])) {
            $save_action = $this->grid['options']['save_action'];
        }

        // Сохраняем урл для режима редактирования
        $this->edit_url = $save_action . '?' . http_build_query($save_action_query);

        // Может быть указан для конкретной колонки урл
        if (!empty($column['editable']['save_action'])) {
            $save_action = string_replace_keys_values_extended($column['editable']['save_action'], $row);
        }

        if(!isset($save_action)){
            return false;
        }

        $attributes = ['autocomplete' => 'off'];

        if (!empty($column['editable']['attributes'])) {
            foreach ($column['editable']['attributes'] as $akey => $avalue) {
                if (is_string($avalue)) {
                    $attributes[$akey] = string_replace_keys_values_extended($avalue, $row);
                } else {
                    $attributes[$akey] = $avalue;
                }
            }
        }

        if (empty($this->grid['options']['deny_mass_editing'])) {
            $this->editable = true;
        }

        // Добавляем ID строки
        $save_action_query['id'] = $row[$id_field];

        if (isset($column['editable']['items']) && is_callable($column['editable']['items'])) {
            $column['editable']['items'] = $column['editable']['items']($field, $row);
        }

        return [
            'component'   => $column['editable']['renderer'] ?? 'form-input',
            'items'       => $column['editable']['items'] ?? [],
            'edit_icon'   => html_svg_icon('solid', 'pen', 16, false),
            'value'       => $row[$field] ?? '',
            'new_value'   => null,
            'has_error'   => false,
            'attributes'  => $attributes,
            'edit_mode'   => false,
            'lang_edit'   => LANG_EDIT,
            'lang_save'   => LANG_SAVE,
            'save_action' => $save_action . '?' . http_build_query($save_action_query)
        ];
    }

}
