Для нашей ERP системы на MODX требуется много редактируемых html-таблиц. Функционал большинтсва таблиц практически одинаков. В основном требуется просто CRUD — создание, чтение, редактирование, удаление записей из базы. Для этой цели имеет смысл создать отдельный компонент или использовать существующий. До написания getTables и PVTables для редактируемых таблиц в MODX был только MIGX на EXTJS 3. EXTJS 3 не понравился мне тем, что для простой задачи выдает кучу HTML кода в котором не легко разобраться. Был сделан getTables на php и jquery. В принципе с большинством задач он справляется, но сейчас я подсел на реактивный фреймворк Vue. И решил сделать таблицы на нем. (Были еще мотивы делать, но по большому счету потому, что понравился Vue :-). Хотя как оказалось, по моим ощущениям, на программировании на нем в среднем уходит больше времени :-().



Задумка PVTables

  1. В базе данных, создаются таблицы, которые надо редактировать. С помощью MIGX или другим способом. Или используем существующие.
  2. В таблице gtsAPI «Таблицы АПИ» создаем краткое описание (иструкции) нужной таблицы.
  3. При вызове сниппета PVTable указываем название таблицы.
  4. Затем спиппет подгужает js код PVTables и передает в него название таблицы. PVTables делает запрос к gtsAPI и подгужает описание таблицы, по которому строит таблицу и делает запросы на чтение и запись в gtsAPI.
Выбрал фреймворк PrimeVue https://primevue.org/setup/, так как в нем для таблиц много сделано.

Быстрый старт
Для самой простой таблицы, в «Таблицы АПИ» нужно только прописать имя таблицы (Если имя совпадает с классом MODX таблицы). Завести и выбрать пакет MODX к которому привязана таблица.


В properties прописать действия доступные для таблицы.


Можно использовать
{	
	"actions":{
		"read":{},
		"create":{},
		"update":{},
		"delete":{},
		"subtables":{
			"table":"tableName",
			"where":{
				"field1":1
			}
		},
		"subtabs": {
			"test": {
				"DocOrderLink": {
					"title": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
					"table": "DocOrderLink",
					"where": {
						"type_order_id": 1,
						"order_id": "id"
					}
				},
				"OrgsContact": {
					"title": "\u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b",
					"table": "OrgsContact",
					"where": {
						"OrgsContactLink.org_id": "loc_org_id"
					}
				}
			}
		}
	}
}
При первом запросе к таблице gtsAPI автоматически сгенерирует описания полей к таблице:
{
    "actions": {
        ....
    },
    "fields": {
        "id": {
            "type": "view",
            "class": "tSkladSmena"
        },
        "date": {
            "type": "date",
            "class": "tSkladSmena"
        },
        "number": {
            "type": "number",
            "class": "tSkladSmena"
        },
        "hour": {
            "type": "number",
            "class": "tSkladSmena"
        }
    }
}
Или удобнее описания сразу сгенерировать кнопкой:



В properties поля можно добавить, удалить и сменить тип. Прописать «label» — название поля и «readonly»:1 — только для чтения. (Потом добавить «filtrer» с кастомным фильтром.)

Преимущества PVTables
1) Можно быстро сделать простую таблицу.
2) Описания таблиц можно, в отличии от getTables, переиспользовать на других страницах сайта или в других таблицах в качестве подтаблиц. Ну и отсюда недостаток ошибка в таблице распространиться везде где она используется.
3) В gtsAPI для таблиц написан стандартный контроллер таблиц, который уже умеет GRUD для произвольных таблиц базы. Нет необходимости для каждой таблицы писать свой контроллер.
4) Можно, в отличии от getTables, переиспользовать описания полей типа autocomplect и select. Что за поля такие ниже в пункте поля PVTables.
5) Часто вместе со стандартным GRUD в таблице еще нужно какое-то кастомное действие. В PVTables можно использовать внешние action — когда PVTables используется в компоненте Vue, сторонний action — когда в gtsAPI действия перенаправляются в сервисный файл компонента, триггеры — до и после стандартный действий можно вызвать функции из сервисного файла компонента, плагины (Думаю внедрить, но еще не определися какие нужны.). Более подробное описание допвозможностей ниже.
6) Для запросов в базу предусмотренны настройки безопасности. В «Таблицы АПИ» можно разрешить доступ только аутенфицированным пользователям, группам пользователей или пользователям у которых есть определенное разрешение MODX. Эти же разрешения можно прописать отдельно для каждого действия в properties:
"actions": {
	"create": {
		"authenticated":1,
		"groups": "ceh_boss,Administrator",
		"permitions":"list"
	}
}
query в формате pdoFetch
Попробовав писать запросы в mysql в формате pdoFetch, я запросы уже по другому и не пишу. gtsAPI по умолчанию формирует запрос в виде:
$default = [
            'class' => $rule['class'],
            'select' => [
                $rule['class'] => '*',
            ],
            'sortby' => [
                "{$rule['class']}.id" => 'DESC',
            ],
            'return' => 'data',
            'limit' => 0
        ]
Если в таблице нужно отобразить данные из нескольких таблиц базы данных, то в properties описания таблицы нужно использовать интсрукцию «query»:
{
    "loadModels": "ExcelRaschet",
    "actions": {
        ...
    },
    "query": {
        "class": "sraschet",
        "where": {
            "sraschet.last": 1
        },
        "select": {
            "sraschet": "sraschet.id,sraschet.family_id,sraschet.manager_id,sraschet.raschet_date,sraschet.loc_org_id,sraschet.object,sraschet.price,sraschet.Profit,sraschet.comment,sraschet.status_id,sraschet.contact_date"
        },
        "sortby": {
            "sraschet.id": "DESC"
        }
    }
}
Удобство pdoFetch в том, что можно на «лету» менять запрос. Например, нужно 2 почти одинаковых запроса с разными where. Написали 1 массив запроса pdoFetch, сделали запрос, затем в массиве переписываем where и делаем второй запрос.
В gtsAPI инструкция «query» сливается с запросом по умолчанию через array_merge. И чтобы изменить сортировку по умолчанию достаточно написать:
{
    "query": {
        "sortby": {
            "id": "ASC"
        }
    }
}
Если в «query» есть «select», то функция автогенерации полей делает описания полей по «select».

Типы полей таблиц
Сейчас заведены типы полей text, textarea, boolean, date, autocomplete, select, (нужно еще html, datetime, file.)
autocomplete и select особые поля.
autocomplete позволяет выбрать вставить в таблицу (в status_id например) id записи из другой таблицы базы данных.


Для того чтобы другая таблица данных могла предоставлять autocomplete в ее описании в properties надо вставить инструкцию «autocomplete»:
{
    "actions": {
        ...
    },
    "autocomplete": {
        "select": [
            "id",
            "name"
        ],
        "where": {
            "name:LIKE": "%query%"
        },
        "tpl": "{$name}",
        "limit": 0
    }
}
Например, в настройках таблицы gsRaschet в properties задано поле
{
	"fields":{
		...
		"org_id":{
			"type":"autocomplete",
			"table":"Orgs"
		}
	}
}
Тогда, чтоб gtsAPI могло выполнять autocomplete инструкцию
    "autocomplete": {
        "select": [
            "id",
            "name"
        ],
        "where": {
            "name:LIKE": "%query%"
        },
        "tpl": "{$name}",
        "limit": 0
    }
Надо вставить в описание таблицы Orgs в properties.

select — какие поля запращивать в mysql. where — в поле автокомплект можно вводить текст и поле будет искать этот текст в базе по инструкции в where. tpl — строка отображаемая в поле в формате fenom (вообще хотелось бы чтоб vue формировал эту строку, но vue, как мне сказали, парсит на этапе компиляции компонента и на этапе работы js в vue уже нет ). limit — если в другой таблице всего 10 строк, то имеет смысл подружать их все сразу, а если там например 2000 контрагентов, то лучше поставить «limit»:20 и не загружать их все сразу.
На основе select, where, limit формируется простой запрос в базу. Если нужно, то запрос можно кастомизировать с помощью инструкции «query». Например выборка пользователей из группы 6:
{
    "actions": {
        "read": []
    },
    "autocomplete": {
        "where": {
            "modUserProfile.fullname:LIKE": "%query%"
        },
        "query": {
            "class": "modUser",
            "leftJoin": {
                "modUserProfile": {
                    "class": "modUserProfile",
                    "on": "modUserProfile.internalKey = modUser.id"
                },
                "modUserGroupMember": {
                    "class": "modUserGroupMember",
                    "on": "modUserGroupMember.member = modUser.id"
                }
            },
            "where": {
                "modUserGroupMember.user_group": 6,
                "modUser.active": 1
            },
            "select": {
                "modUser": "modUser.id",
                "modUserProfile": "modUserProfile.fullname"
            },
            "sortby": {
                "fullname": "ASC"
            }
        },
        "tpl": "{$fullname}",
        "limit": 20
    }
}
Еще в autocomplete можно добавить инструкцию «default_row» — в формате pdoFetch where для определения строки автокомплета по умолчанию. Вернет в ответ сервера «default»:nomer_stroki.
(parent позже. Надо найти где он вообще использовался.)

Поле select выдает (почти) обычный селект. Только описания доступных опций селекта забиваются таблице «Селекты» в gtsAPI для того, чтобы их можно было переиспользовать в разных таблицах.


В «Опции в JSON» опции можно забить либо:
Мужчина,Женщина
Либо:
[
	[1,"Прямоуголка"],
	[2,"Кругляк"]
]
Для select нужно прописать имя поля.

Для autocomplete можно пописать имя поля автокомплект.


Тогда функция автогенерации описаний полей автоматически по имени поля поймет, что полю нужно поставить тип autocomplect и пропишет нужную таблицу.


Также нужный тип поля select прописывается из имени поля селекта.

Допвозможности. Сторонний action
Часто вместе со стандартным GRUD в таблице еще нужно какое-то кастомное действие. Например перерасчет остатков на складе:


По задумке можно написать такой action:
{
    "loadModels":"skladmake",
    "actions": {
        "skladmake/pereraschet_sklad": {
              "groups": "ceh_boss,Administrator",
		"head":true,
		"icon":"pi pi-trash",
		"class":"p-button-rounded p-button-danger"
        }
    }
}
«head»:true — вывести кнопку в топ таблицы. «row»:true — вывести кнопку в строке таблицы.
gtsAPI уже поддерживает такие действия, а PVTables еще нет. В gtsAPI такие действия перенаправляются в сервисный файл компонента. В смысле вызывается фанкция handleRequest из файла "/core/components/skladmake/model/skladmake.class.php".

Допвозможности. Триггеры
В gtsAPI при каждом запросе подгружеется класс сервисного файла пакета. Например, "/core/components/$package/model/$package.class.php". Если такой файл есть. Это либо пакет к которому привязана таблица, либо имена пакетов можно задать в «loadModels».

Если в $package.class.php заданна функция regTriggers:
public function regTriggers()
    {
        return [
            'DocOrderLink'=>[
                'gtsfunction2'=>'triggerDocOrderLink',
            ],
        ];
    }
то тогда когда gtsAPI выполняет какие-то стандартные действия с таблицей класса 'DocOrderLink' gtsAPI вызывает функцию 'triggerDocOrderLink' до и после выполнения этих действий.
(В принципе это почти плагины MODX только мне удобней сразу писать их в коде файла компонента.)
Например, такой триггер:
public function triggerDocOrderLink(&$params)
{
	if($params['type'] == 'after' and $params['method'] == 'read'){
		// при запросе к таблице запрещаем чтение всем кроме администратора
		if($this->modx->user->id != 1) return $this->error("Доступ только администратору");
	}
	if($params['type'] == 'after' and $params['method'] == 'update'){
		// при изменение строки таблицы записываем в нее дату изменения
		$params['object']->date_update = date('Y-m-d');
		$params['object']->save();
	}
	return $this->success('');
}
Допвозможности. Внешний action
Внешний action используется тогда, когда при нажатии на кнопку нужно показать юзеру какой-то диалог. Тогда пишем компонент vue. Рекомендую сделать копию ExcelRaschet:
npm run copy
Ставим пакет PVTables:
npm i pvtables
в src/App.vue что-то типа:

<template>
  <PVTables table="sraschet" 
    :actions="actions" 
    ref="childComponentRef"
  />
  <Toast/>
</template>

<script setup>

  import { PVTables } from 'pvtables/pvtables'
  import { ref } from 'vue';
  import Toast from 'primevue/toast';

  import apiCtor from 'pvtables/api'
  import { useNotifications } from "pvtables/notify";

  const childComponentRef = ref()
  const createDocDialog = ref(false)
  const updateDocDialog = ref(false)
  
  const api = apiCtor('DocOrderLink')

  const { notify } = useNotifications();
  
  const actions = ref({
    DocOrderLink:{
      create:{
        head:true,
        icon:"pi pi-plus",
        class:"p-button-rounded p-button-success",
        head_click: async (event,table,filters,selectedlineItems) => {
          ...
        }
      },
      update:{
        row:true,
        icon:"pi pi-pencil",
        class:"p-button-rounded p-button-success",
        click: async (data, columns,table,filters) => {
          ...
        }
      }
    },
    
  })

</script>
Более подробный пример в https://github.com/touol/ExcelRaschet. (Но и там запутано скоро будет :-()
Для записи в таблицы можно использовать:
import apiCtor from 'pvtables/api'
const api = apiCtor('DocOrderLink')
try {
  await api.create(DocLink.value)
} catch (error) {
  notify('error', { detail: error.message });
}
Для вызова стореннего action:
const api_sraschet = apiCtor('sraschet',600000)
try {
  const resp = await api_sraschet.action('ExcelRaschet/createAccountIn1c',data.value)
} catch (error) {
  notify('error', { detail: error.message });
}
В описании таблицы «actions» описывает одновременно и доступные в gtsAPI действия и одновременно описывает какие действия выводить в таблице PVTables. Для того чтобы, действия не показывать, но чтоб они были доступны добавлена инструкция «hide_actions»:
{
    "loadModels": "ExcelRaschet",
    "actions": {
        "read": []
    },
    "hide_actions": {
        "ExcelRaschet\/createAccountIn1c": []
    }
}
В если компонент vue на основе ExcelRaschet, то там делаем npm run build и в modx прописываем сниппет:
{'!mixedVue' | snippet:[
    'app'=>'ExcelRaschet'
]}
подтаблицы и подтабы
Ну и остались подтаблицы и подтабы. Я так их называю. В терминологии PrimeVue это expandedRow c таблицей и expandedRow c табами.
Действия вида:
{	
	"actions":{
		"subtables":{
			"table":"tableName",
			"where":{
				"field1":1
			}
		},
		"subtabs": {
			"test": {
				"DocOrderLink": {
					"title": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
					"table": "DocOrderLink",
					"where": {
						"type_order_id": 1,
						"order_id": "id"
					}
				},
				"OrgsContact": {
					"title": "\u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b",
					"table": "OrgsContact",
					"where": {
						"OrgsContactLink.org_id": "loc_org_id"
					}
				}
			}
		}
	}
}
Показывают в строке таблицы кнопки такого вида:


При нажатии на них под строкой таблицы показывается, для subtables, другая таблица. Для subtabs набор табов с таблицами:


Инструкция фильтрует в них строки те что связанны с основной таблицей.
"where": {
	"type_order_id": 1,
	"order_id": "id"
}
«type_order_id»: 1 — фильтровать только документы для расчетов.
«order_id»: «id» — id строки основной таблицы. Если кликаем на кнопке в строке с id=41924, то PVTables отфильтрует строки, где order_id — ид расчета равен 41924.

Так же удобно вывести на 1 странице сразу несколько таблиц в табах:
{'!PVTabs' | snippet : [
    'tabs'=>[
        'DocType'=>[
            'title'=>'Типы документов',
            'table'=>'DocType',
        ],
        'DocTypeOrder'=>[
            'title'=>'Типы заказов',
            'table'=>'DocTypeOrder',
        ],
    ]
]}
Раскраска строк таблиц
Часто надо выделить строки таблиц каким-то цветом по каким-то условиям. Сейчас для этого предусмотренна 1 инструкция (в properties описания таблицы), которая присваивает строкам таблиц классы css:
    "row_setting": {
        "class": "{switch $status_id}{case 5}canceled{case 4}hit{case 3}outwork{case 1}work{\/switch}"
    },
Инструкция снова использует fenom. Хотелось бы прокинуть из properties в js PVTables функцию, но это вроде небезопасно :-(.

Ну пока все :-)
22 июня 2024, 05:38    Александр Туниеков Компоненты 0    3 0

Комментарии ()

    Вы должны авторизоваться, чтобы оставлять комментарии.

    Вы можете авторизоваться на сайте через:
    YandexGoogle