Всем привет! Сегодня добавил очень полезные методы в компоненты фронт-редактора - canBeParent(parent) и canBeChild(child). Они предназначены для того, чтобы решать какие компоненты могут быть дочерними или родительскими для других. Это очень важная и полезная функция, так как не всегда отдельные компоненты могут быть вложенными для других. Для примера можно взять EditableObject. К нему прилагаются еще два отдельных компонента - DefaultView и EditableView (они описаны в статье про EditableObject). Изначально было рассчитано, что эти два компонента могут быть добавлены дочерними только непосредственно в EditableObject и больше никуда. В свою очередь в EditableObject могут быть добавлены и любые другие допустимые компоненты. Вот если не учитывать эти правила в визуальном редакторе и никак не ограничивать, то пользователь может какие угодно компоненты добавлять в какие угодно компоненты и в лучшем случае они или не будут работать (или работать неправильно), а в худшем случае возникнет фатальная ошибка. И вот эти методы как раз созданы для решения этой задачи. В DefaultView прописано (как и в EditableView): То есть для этих компонентов родительскими могут быть только производные от EditableObject и никакие другие. При чем посмотрите внимательней на основные методы: canBeParent(parent) по умолчанию возвращает parent.canBeChild(this), то есть родителем может быть только тот компонент, для которого этот компонент может быть дочерним, то есть выполняется обоюдная проверка и для родительского компонента, и для дочернего по отношению к нему, что позволяет проверять как в родительском, так и в дочернем. Напомню, что можно легко в редактор добавляться свои кастомные компоненты и этими методами вы можете всяко рулить иерархию компонентов. Проверки могут быть прописаны какие угодно, включая по уровням доступов отдельных пользователей (к примеру, в этот компонент админы могут дочерние закидывать, а другие пользователи нет), можно по наличию отдельных дочерних компонентов в родительском и т.д. и т.п. (на сколько хватает фантазии). А в редакторе я сделал, что при перетаскивании нового компонента, у всех имеющихся на странице компонентов, которые не могут быть родительскими для него, пропадает бордер, в результате чего на странице выделенными остаются только те, куда можно бросить новый компонент. То же самое касается и VerticalTimeline. Для него дочерним может быть только VerticalTimelineItem и никакой более. В свою очередь VerticalTimeline может быть добавлен в любой другой разрешенный компонент. Вот как это выглядит: https://youtu.be/uSt9quBuC-o. Согласитесь, так гораздо удобней:) UPD: Вот получился отличный кейс. На проекте есть различные формы подписок, у которых один и тот же контейнер. Контейнер оформлен в отдельный компонент (потому что если в нем что-то поменяется, должно поменяться для всех форм), а сами формы тоже оформлены в самостоятельные компоненты. При этом без контейнера эти формы не должны существовать, то есть добавить их можно только в такой контейнер. Но тут еще момент: в одном контейнере может находиться только одна форма. То есть если уже есть в нем форма, другую добавить нельзя. Вот как это выглядит в действии: https://youtu.be/2DBusXVlQR4
Всем привет! Продолжаю улучшать фронт-редактор, на сколько это у меня получается:) 1. Поправил страницу шаблонов Теперь нормально работает постраничность и фильтры. Можно, к примеру, сделать выборку всех шаблонов по отдельно взятому проекту: https://prisma-cms.com/templates?templates-filters=%7B%22PrismaProject%22%3A%7B%22domain%22%3A%22front-editor.prisma-cms.com%22%7D%7D или все шаблоны, созданные не мной: https://prisma-cms.com/templates?templates-filters=%7B%22CreatedBy%22%3A%7B%22username_not%22%3A%22Fi1osof%22%7D%7D или все шаблоны на основании конкретного компонента, к примеру, Query: https://prisma-cms.com/templates?templates-filters=%7B%22component%22%3A%22Query%22%7D&mkplace-templates-filters=%7B%22component%22%3A%22Query%22%7D Эти же фильтры теперь и в режиме редактора нормально работают http://joxi.ru/Drlz7MqcV3oDl2. То есть если вы хотите на странице вставить уже имеющийся готовый шаблон, вы его сможете найти без особых усилий. 2. В режиме редактирования левая панель с каталогом шаблонов по умолчанию закрыта Недавний комментарий с жалобой на плохую производительность был услышан и теперь панель с каталогом шаблонов стала открываемой и по умолчанию скрыта. http://joxi.ru/L21WpJ8IRL9kxr Функция "Выбрать готовое и вставить" действительно не очень часто используется, потому имеет смысл открывать эту панель только тогда, когда это нужно. 3. Более толстый бордер для самостоятельных шаблонов в режиме редактирования Я не раз говорил ранее, что отдельные части шаблонов можно сохранять в самостоятельные и использовать повторно в других шаблонах. Отдельно писал про это здесь (см второй раздел публикации). Но проблема была в том, что визуально на редактируемой странице не было понятно какие из элементов на странице самостоятельные (так называемые "корневые"), а какие нет. Вот эту проблему, как мне кажется, я решил увеличением бордера таких элементов до 2px. http://joxi.ru/52anNeOiEvoyWA. Теперь все видно.
Всем привет! Сегодня добавился новый элемент, точнее два: VerticalTimeline и VerticalTimelineItem. Используется он для того, чтобы вертикально выводить какие-нибудь сущности с привязкой ко времени (хотя привязка не обязательна). За основу был взят vertical-timeline-component-for-react. Пример использования можно увидеть здесь: https://front-editor.prisma-cms.com/templates/cjwidh5xcrow20a89w1ms6ahi Вообще этот компонент интересно изучить с точки зрения кастомизации фронт-редактора. Прежде чем запилить его в ядро, я сделал сначала вот такое на другом проекте: https://youtu.be/9tT4tKdjD38. Всего за два часа! И это включая добавление кастомного типа объектов в базу данных, резолверы, добавление запросов на создание/редактирование/вывод данных, добавление раздела на сайте и выбор реакт-компонента для рендеринга (из изученных больше всех понравился именно vertical-timeline-component-for-react, хотя и он не идеален и я отправил 3 багрепорта). При чем в процессе разработки раскрыл некоторые незадокументированные возможности фронт-редактора, о которых и сам не очень догадывался :) К примеру, как оказалось, весьма не сложно кастомизировать активную область компонента: http://joxi.ru/KAgY7JySE9eaom. Мне так хотелось, чтобы визуально было понятно куда кидать внутренние компоненты, так как лейбл в моем случае редактировался в отдельном поле. Вот пример кода. Здесь есть еще одна причина почему корневой элемент надо было вывести именно так. Дело в том, что сторонний компонент не умеет на себя принимать какие-либо свойства кроме тех, что там прописаны (что не есть правильно ИМХО). Соответственно все, что требуется в режиме редактирования (события клика и наведения, Drag n Drop и т.п.) - все это игнорировалось. При этом данный компонент должен быть прямым потомком для родительского, иначе у него терялась логика с пробрасываемыми в него свойствами. А так получилось и сторонний компонент в прямых потомках вывести, и свои кастомные свойства отрендерить. Во-вторых, при выводе в цикле, мне захотелось, чтобы прошедшие этапы лейбл был одного цвета, будущие другое, а текущий этап - третьего. http://joxi.ru/KAgY7JySE9V7zm. Подводный камень заключается в том, что нет прямой идентификации, что это текущий этап. По сути текущий - это один из тех, дата старта которых меньше, чем сейчас, просто это последний элемент из таких. А в процессе итерации каждый отдельный элемент выводится в самостоятельном компоненте и просто так всего массива элементов нет в наличии. Но этот массив можно получить через родителя. В объекты класса Iterable передается массив items и его можно получить через свойство parent и в этом массиве уже найти текущий элемент. Вот пример кода: Данный код просто для примера, потому что в текущей реализации данной логики нет (ибо нет смысла запиливать под какую-то определенную структуру данных). В текущей реализации логика чуть другая: Первый дочерний элемент уходит в лейбл, а остальное все в основную область. http://joxi.ru/vAWlRe5tg6yyLr И вот с этим пришлось попыхтеть... Хотелось не просто выводить в лейбл первый компонент только в режиме штатного рендеринга, но и хотелось, чтобы его можно было редактировать. А для этого надо было разделить имеющийся массив дочерних компонентов и вывести их в двух разных местах, при чем на входе у нас не готовые react компоненты, а просто JSON-данные. Для этого пришлось довольно большой коммит в ядро отправить. В итоге в лейбл я вывожу первый дочерний компонент через новый метод this.renderComponent(component), который позволяет делать рендер дочерних элементов просто с передачей JSON-данных, а в основную область я отсекаю первый элемент из массива дочерних. Только в этих случаях важно не нарушать целостность исходного массива дочерних компонентов, иначе попрут коллизии. Если вдруг вы захотите тоже подобным образом поиграться, напоминаю, что собственный кастомные элементы добавлять несложно, вот подробная статья.
Всем привет! Сегодня столкнулся вот с какой ситуацией: на одном из проектов при импорте данных из excel-файла (заказы и товары в заказах) ощутилась весьма серьезная нагрузка на странице браузера. Стал разбираться и выяснил, что во фронте для интерактивности навешана подписка на изменения данных с сервера и когда на сервере создается или обновляется заказ или позиция в заказе, в браузер по веб-сокету прилетает уведомление и там происходит переподгрузка данных и прочая магия. А тут загружается ~150 заказов и порядка 1000 позиций, то есть это более 1000 уведомлений в браузер и реинициализаций на странице (конечно же столько выполняется только у админа, так как простые пользователи получают уведомления только по своим заказам, но тем не менее, даже для одного пользователя такого происходить не должно). Стал разбираться. Отметил, что хотя в ответ на уведомления на сервер отправлялось совсем немного запросов на подгрузку данных (что было и раньше), нагрузка все же была и она явно связана с недавним новшеством в @prisma-cms/front - использованию API-схемы, подгружаемой с сервера. Схема эта довольно большая (в несжатом виде порядка 2Мб), а главное - в кеш apollo-client добавляется порядка 30 000 ключей со значениями. И получилось, что при обновлении данных, обновлялась и API-схема, что в свою очередь и давало большую нагрузку на вычисления (аполло-клиенту же надо проверить, что есть в кеше, а чего нет, получить данные, обработать, сравнить, закинуть новые данные в кеш и т.п.). В общем, пришлось повозиться, но исправить ситуацию получилось. Теперь на сервере в режиме SSR эта схема запрашивается один раз, после чего полученная схема переводится в JSON-строку и сохраняется в переменную, которая в дальнейшем уже при каждом запросе добавляется в документ без повторных запросов к API-серверу и преобразований этой схемы. В браузер загружается HTML-документ уже с этой схемой и на сервер более запросов не выполняется. В итоге любая операция с аполло-клиентом и кешем никак не затрагивают не касаются схемы и нагрузка сильно упала. Теперь 1000 уведомлений в минуту об обновленных данных и реагирование на них - не проблема. По поводу размера HTML-документа: хотя схема в исходном виде и весит метра два (а плюс к этому еще и сам HTML-документ с готовым кешем API-данных), реальный объем передаваемых данных значительно меньше за счет gzip-сжатия на уровне nginx. В моем случае при реальном размере страницы в 3Мб в сжатом виде это всего 102кб. Но помимо производительности тут еще и правильность работы значительно улучшилась. Дело в том, что от этой схемы зависит работа нескольких компонентов (в частности фильтры @prisma-cms/filters и Query из @prisma-cms/front-editor). Если не будет схемы, то они не смогут получить данные и в таких случаях они просто ничего не выводят. И вот получалось, что когда выполнялось обновление данных, возникали ситуации, когда хоть и на секунду-другую, но схема пропадала, из-за чего отдельные части страницы пропадали, что совсем нехорошо. Теперь такой ситуации не возникает. Сильно в дебри не буду лезть, но если кому интересно, изменения на уровне этих двух коммитов: https://github.com/prisma-cms/boilerplate/commit/39a0677c6b444c623effeb27d9053b3abdc34c05 https://github.com/prisma-cms/front/commit/3411093c75e806048fa65443e020df56b997eba4
Всем привет! Сегодня я хотел бы рассмотреть один кейс, который хоть и не очень распространенный, но все же встречается, а его реализация требует поднапрячь мозги. Вот и сейчас я опять с ним столкнулся и решил более ответственно подойти к реализации, а заодно и задокументировать это. Задача: добавить на уровне GraphQL фильтр по нескольким полям, к примеру, по юзернейму, имени и емейлу. В штатном режиме запрос у нас бы выглядел примерно вот так: Уточню, что при выполнении этого запроса мы должны получить пользователей, у которых имя, юзернейм или емейл содержат заданную строку. Иногда такое перечисления полей в интерфейсах не очень удобно, тем более для простых сотрудников, которые не хотят заполнять несколько отдельных полей (в нашем случае username, fullname, email), а хотят заполнять одно поисковое поле и чтобы по нему "все искалось". При чем логика бывает более сложная (к примеру, заказы по номеру или владельцу, у которого имя или емейл совпадает С). Но GraphQL просто так ничего не принимает лишнего в запросах. Если вы хотите передавать какое-то новое поле в запросе, вы его должны описать в схеме и заставить сервер обрабатывать этот запрос. Вот решение для подобных случаев я и хочу описать. 1. Добавляем свое поле в API-схему В файл схемы user.graphql дописываем Напоминаю, что при сборке API-схемы командой yarn build-api выполняется суммирование всех объектов схемы, таким образом на выходе мы получим UserWhereInput не только с полем search, но и с другими полями, описанными ранее, то есть фактически мы именно добавляем поле, а не просто объявляем новый объект. 2. Расширяем резолвер Как я говорил ранее, API состоят из двух частей: 1. Низкоуровневое, генерируемое самой призмой (и которое обрабатывается в docker-сервере). 2. Внешнее общедоступное API. Так вот, сейчас мы добавили новое поле search в наше фронтовое API и оно в запросе это поле примет. Но далее запрос улетает на низкоуровневое API, а там об этом поле ничего не известно, и в случае отправки такого запроса туда, мы получим ошибку от сервера, что такое поле в схеме не описано и запрос выполнить нельзя. Но мы сейчас и не будем пытаться заставить низкоуровневое API понимать поле search, наша задача в другом: приняв search на входе, отправить на низкоуровневое API измененный запрос, а именно с условием: в то время как на вход (во внешнем API) мы будем принимать запрос попроще: То есть в нашем резолвере мы должны, получив условие с полем search, удалить его из запроса, вместо него подставить измененное условие и отправить запрос далее. Вот здесь я переопределяю два резолвера (users и usersConnection). Прежде чем отправить запрос далее, я вызываю метод addQueryConditions и передаю в него текущие условия параметры запроса. Вот этот метод: Он в свою очередь берет из контекста метод modifyArgs и передает в него модификатор this.injectWhere. Вот его код для наглядности: В принципе, хоть и несколько запутанно написано, но на самом деле здесь всего понимать не надо и достаточно только сосредоточиться на написании своего метода injectWhere, а он не особо сложный. А вот результат для примера: https://prisma-cms.com/people?filters=%7B%22search%22%3A%22test%22%7D Что хорошо в данной реализации, так это то, что она работает на любом уровне вложенности запроса и даже в массивах условий (AND и OR). Вот пример: https://prisma-cms.com/people?filters=%7B%22Resources_some%22%3A%7B%22CreatedBy%22%3A%7B%22search%22%3A%22test%22%7D%7D%7D А еще этот метод за раз можно несколько раз вызывать (если вы написали несколько отдельных модификаторов, а не прописали все условия в одном). Но если и минус, который скорее всего не будет решен. Дело в том, что мы добавили кастомное поле search в объект UserWhereInput. А этот объект используется не только в выборках пользователей, но и в запросах других объектов, связанных с пользователем, к примеру, в ресурсах. То есть если мы во фронте пропишем вот такой запрос: то синтаксических ошибок мы не получим, технически здесь все ОК. Но при выполнении запроса мы получим ошибку Потому что в бэк-API такое поле неизвестно. Для решения этой проблемы придется еще и модификатор писать для ресурс-запросов. Но в целом эта проблема не особо критична, о ней просто надо знать (чтобы не выполнять). Но для самостоятельных запросов метод очень полезный.
P.S. Кстати, а почему кнопка "Редактировать шаблон" отображается у всех, включая не-админов? Забыл тебе ответить :) Это я оставил специально, чтобы было видно редактор. А то итак не понятно что это такое, еще и видно не будет :) Но это отключается. В вызов RootConnector передай параметр clonable={false}. Тогда выводиться будет только для судо и для создателя шаблона.
На счет производительности: есть такое понятие как преждевременная оптимизация. Вот здесь я ее пока избегаю. Весь этот функционал - сплошной эксперимент. Когда я его начинал, у меня были идеи и потребности, но до сих пор нет окончательной мысли как это будет выглядеть в конечном итоге. Нет смысла оптимизировать то, что постоянно меняется и переделывается. Но несколько причин проблемы с производительностью мне известны: 1. React dev tools. На гитхабе не мало issue создано на счет низкой производительности. Скорее всего он использует в работе deep clone + lodash или типа того. react context позволяет передавать любые объекты, в том числе и классы компонентов. И вот так как в @prisma-cms в контекстах довольно много всякого, dev tools становится очень грустным и жутко тормозит. По этой причине он у меня давно уже отключен. 2. Довольно неоптимальный базовый класс компонентов фронт-редактора. Как я и говорил в статье, там не просто this.props используется, а своя костыльная логика, и скорее всего он так же дает лишней нагрузки (но по большей степени только в режиме редактирования. В обычном режиме там особой потери нет). Но повторюсь, когда логика устаканится, займусь его перепиливанием. 3. Работа со строками. Вот глянь вот эту статью: https://habr.com/ru/post/449368/. Уверен, в итоге придется и на этот счет серьезно прошерстить код. В общем, да, проблемы с производительностью есть, но как и говорил, позже буду их обязательно решать, уверен, можно будет значительно поправить производительность. Сейчас же в режиме редактирования (тем более в dev-режиме, а не в production mode), надо и памяти не мало (в dev надо рассчитывать на гиг-два на вкладку), и процессор помощнее. У меня core i7 и 14Gb оперативы, относительно комфортно себя ощущаю. На счет непонятных фреймов: вот здесь я писал, что отдельные части шаблонов можно сохранять в самостоятельные сущности и потом использовать на странице. Все их можно увидеть на странице /templates. Вот в редакторе в левой панели они и выводятся. Это для того, чтобы можно было вставить уже готовые блоки на странице. Это такой своего рода minecraft - создаешь отдельные небольшие куски, их можно использовать в формировании более крупных шаблонов, те еще и так далее. При чем там есть важная особенности при вставке: если просто кликнуть какой-либо готовый шаблон, то будет на странице вставлен весь его код (то есть скопирован). А если кликнуть кнопку "Вставить с ID (Insert with ID)" http://joxi.ru/D2PeVyqhqY6KZA, то будет вставлена ссылка на этот шаблон. То есть можно в нескольких частях страницы и на разных страницах повставлять ссылки на готовый шаблон, а потом редактировать в одном месте, и везде будет выводиться актуальный код. (это частичный ответ и на пункт 2 твоего вопроса). >> Кнопочки на компонентах, что означает та или иная, ну и т.д. Для этого я и сделал отдельный сайт, чтобы описать все основные компоненты фронт-редактора. В ближайшем будущем прям в кнопки добавлю ссылки на статьи, описывающие эти элементы. И для этого я и примеры привожу, чтобы было видно на более локальных шаблонах что как работает. Постепенно документация будет на каждый элемент. Ну а так, вопросы задавайте или экспериментируйте :) И смотрите исходный код компонентов. Подключенные компоненты перечислены здесь, оттуда можно найти путь до кода каждого компонента в отдельности, чтобы изучить непосредственно его код.
Фишка с редактором шаблонов классная. Учитывая, что у приложений и сайтов зачастую большинство компонентов на странице переиспользуемые, то визуально размещать их куда удобнее. Только тут есть несколько моментов, которые меня очень смущают и заставляют задуматься о выпиливании этого инструмента: 1) В первую очередь, это то, что он кушает много ресурсов процессора. Вот я кликаю на каком-то компоненте, у меня открывается слева панель с непонятными фреймами и 1.4 гигагерцовый процессор моей двенашки начинает мучаться. Не пойму, с чем это связано, но это заметно на прокрутке этого блока слева. 2) Я не совсем понимаю его интерфейс, в первую очередь вот этот выплывающий сайд слева. Что он делает, для чего он? Кнопочки на компонентах, что означает та или иная, ну и т.д. P.S. Кстати, а почему кнопка "Редактировать шаблон" отображается у всех, включая не-админов?