Содержание
- Архитектура JS-модуля
- Backend-часть JS-модуля
- Frontend-часть JS-модуля
- Требования к JS-модулям
- Точки встраивания (targets) и доступные в них данные
- Доступные Vue-компоненты
- Разработка JS-приложения
- Возможности и ограничения JS-модулей
- 1. Ограничения на значения атрибутов элементов в шаблонах Vue-компонентов
- 2. Выполнение HTTP-запросов
- 3. Навигация и ссылки
- 4. Работа с пользовательскими полями
- 5. Работа с действиями
- 6. Работа с ссылками на страницы системы
- Генерация ссылок на страницы системы
- Перенаправление пользователя на определенную страницу системы
- Встраивание в интерфейс системы
- Публикация frontend-части
JS-модули позволяют расширить возможности интерфейса системы.
С помощью frontend-части модуль может добавлять элементы в существующие разделы CRM, открывать собственные страницы и взаимодействовать с интерфейсом через API хоста.
Архитектура JS-модуля
JS-модули является расширенной версией стандартных модулей Маркетплейса. Отличием является то, что они состоят из двух частей: backend-часть и frontend-часть.
Важно!
На Github представлен полноценный пример реализации модуля с backend-частью на Symfony @retailcrm/specialist-booking.
Также доступна библиотека примеров @retailcrm/core-ui-extensions-examples.
Backend-часть JS-модуля
Backend-часть JS-модуля разрабатывается также, как и у стандартных модулей. В ее подготовке вам помогут статьи про основные требования к модулям, подключение и активацию модуля и добавление модуля в Маркетплейс.
Backend-часть модуля должна как минимум содержать логику по подключению и активации модуля в Маркетплейсе.
Если frontend-часть модуля будет обращаться к backend-части, то последняя помимо базовой логики должна также предоставлять ендпоинты для обработки запросов, отправляемых из frontend-части.
Важно!
После подготовки backend-части модуля заведите его в разделе «Модули» партнерского кабинета. Если модуль содержит frontend-часть, включите JS-функциональность в карточке модуля по актуальной инструкции для партнерского кабинета.
Frontend-часть JS-модуля
Frontend-часть JS-модуля должна представлять собой приложение, написанное на Javascript или TypeScript и Vue 3. Приложение будет выполняться в изолированной среде iframe. Выполнение приложения производится с помощью библиотеки удаленного рендеринга @omnicajs/vue-remote. Связь между приложением и основным интерфейсом системы (далее «хостом») осуществляется через API хоста и postMessage.
В актуальном состоянии платформы используются два сценария запуска frontend-части JS-модуля.
1. Модули через v1-endpoint и runner: "worker"
Для новых JS-модулей рекомендуется использовать @retailcrm/embed-ui-v1-endpoint/remote. В этом сценарии frontend-часть запускается через script-entrypoint и может одновременно содержать:
- виджеты в
targets; - собственные страницы модуля.
При регистрации frontend-части для такого сценария необходимо указать runner: "worker".
2. Модули на iframe
Сценарий с runner: "iframe" является legacy-режимом и не рекомендуется для новых модулей. Он используется для существующих модулей, которые инициализируются через createWidgetEndpoint(...), а точка входа публикуется как HTML-страница.
В будущем поддержка legacy-режима iframe может быть прекращена.
Страницы модулей доступны, начиная с версии 0.9.11 embed-ui.
Подробное описание работы со страницами можно найти в статье «Создание собственных страниц JS-модулей». Навигация, query string и переходы между страницами описаны в статье «Навигация и маршрутизация в JS-модулях».
К JS-приложениям предъявляются определенные требования и они содержат ограничения на используемый функционал Vue и JS, о чем описано подробнее ниже.
Требования к JS-модулям
1. Разрешенные компоненты для вывода на странице
В случае, когда JS-модуль добавляет функциональность на существующие страницы системы, непосредственно на странице (например, странице заказа) JS-модулям разрешено выводить только компоненты UiToolbarButton и UiToolbarLink из предоставляемых готовых Vue-компонентов в количестве до 2 штук в рамках одного target-а.
Подробнее с компонентами можно ознакомиться в витрине компонентов.
2. Отображение дополнительного контента
JS-модуль по клику на кнопку или ссылку может отображать дополнительные данные, формируемые самостоятельно или подгружаемые из сторонних систем. Для вывода этой информации предпочтительно использовать Vue-компонент боковой панели UiModalSidebar. В случае, если контент достаточно широкий: табличные данные, карта и другая подобная информация — допустимо использовать Vue-компонент модального окна UiModalWindow.
Для крупных сценариев, где нужен полноценный экран внутри CRM, следует использовать собственные страницы модуля. Подробно этот сценарий описан в статье «Создание собственных страниц JS-модулей».
Подробнее с компонентами можно ознакомиться в витрине компонентов.
3. Требования к страницам
Если модуль публикует собственные страницы, для них следует использовать отдельную конфигурацию pages и сценарий выполнения через @retailcrm/embed-ui-v1-endpoint/remote.
При регистрации frontend-части модуля со страницами необходимо указать runner: "worker". Без этого собственные страницы модуля не будут работать в актуальном сценарии запуска.
Страницы подходят для сценариев, где модулю требуется:
- отдельный экран внутри CRM;
- собственная структура интерфейса;
- работа с меню;
- навигация и query string внутри сценария страницы.
Подробное описание работы со страницами можно найти в статье «Создание собственных страниц JS-модулей».
4. Локализация JS-модуля
Все тексты и надписи, которые выводятся во frontend-части JS-модуля, должны быть представлены на 3-х языках: русском, английском и испанском, и выводиться в той локали, которая задана в аккаунте системы.
В качестве библиотеки для организации переводом рекомендуется использовать vue-i18n. В примерах JS-модулей @retailcrm/core-ui-extensions-examples вы можете найти реализацию переводом надписей и выставление локали в соответствии с локалью аккаунты системы.
5. Вывод изображений в JS-модуле
Для вывода изображений требуется использовать Vue-компонент UiImage и указывать свойство resize, чтобы отображалась уменьшенная версия изображения для экономии трафика и ускорения загрузки изображений.
Если нужно вывести аватарку, то используйте специальный Vue-компонент UiAvatar.
Точки встраивания (targets) и доступные в них данные
Приложение может встраиваться в определенные точки интерфейса (в коде они называются targets) и оперировать доступными в этих точках данными.
При подготовке JS-приложения вам нужно определиться с перечнем точек встраивания.
В разных точках доступен разный набор данных. Данные реактивные, то есть изменения применяются ко входным объектам при их изменении в интерфейсе (в частности в формах, например, форме заказа, форме клиента и т.п). Некоторые поля мутабельные, то есть их можно изменять в JS-приложении и эти изменения применятся к форме. Такие поля в справочнике отмечены как readonly: false.
Для получения объекта из контекста и полей из объекта, используйте функции из @retailcrm/embed-ui/index.d.ts, например:
import { useField } from '@retailcrm/embed-ui'
import { useContext as useSettings } from '@retailcrm/embed-ui-v1-contexts/remote/settings'
import { useContext as useOrder } from '@retailcrm/embed-ui-v1-contexts/remote/order/card'
const order = useOrder()
const address = useField(order, 'delivery.address')
const settings = useSettings()
const locale = useField(settings, 'system.locale')
Доступные Vue-компоненты
Ряд Vue-компонентов, на которых строится интерфейс системы, доступен для JS-модулей. Вы можете использовать их для создания интерфейса JS-модуля.
Подробная информация о возможностях конфигурации компонентов доступна в витрине компонентов.
Доступны props и слоты (дефолтные и именнованные). Для каждого компонента они описаны в витрине.
Часть ограничений связана не только с embed-ui, но и с remote-rendering слоем @omnicajs/vue-remote. Актуальные ограничения нужно учитывать точечно:
- методы host-компонентов доступны только если они явно описаны на стороне remote-компонента, например через
defineRemoteMethod; scoped slot propsот host-компонентов не передаются в remote-слоты;refна native-элементах указывает на proxy-узел remote-дерева, а не на реальный DOM-элемент браузера;- event modifiers поддерживаются при настройке сборки через
@omnicajs/vue-remote/webpack-loaderили Vite plugin; v-modelна native-элементах поддерживается вvue-remote;- для
Transitionне следует закладываться на поддержку без проверки в используемой версии runtime.
Также доступны события. Актуальный список событий можно посмотреть тут.
Для использования добавьте пакет компонентов в package.json вашего модуля:
npm i --save @retailcrm/embed-ui-v1-components
или
yarn add @retailcrm/embed-ui-v1-components
После чего в файле вашего компонента импортируйте нужный компонент и используйте:
// cases/someModule/SomeExtension.vue
<template>
<UiToolbarButton>
{{ t('click') }}
</UiToolbarButton>
</template>
<script lang="ts" setup>
import { UiToolbarButton } from '@retailcrm/embed-ui-v1-components/remote'
//...
</script>
Разработка JS-приложения
В приложении нужно написать Vue-компонент(-ы) JS-приложения модуля. Например:
// cases/phoneReactive/PhoneReactiveExtension.vue
<template>
<UiToolbarButton v-if="phone" @click="phone = t('callMade')">
{{ t('callOn') }}{{ phone }}
</UiToolbarButton>
</template>
<script lang="ts" setup>
import {
useOrderCardContext,
useField,
useSettingsContext,
} from '@retailcrm/embed-ui'
import { UiToolbarButton } from '@retailcrm/embed-ui-v1-components/remote';
import { useI18n } from 'vue-i18n'
import { watch } from 'vue'
// set locale
const settings = useSettingsContext()
const locale = useField(settings, 'system.locale')
settings.initialize()
const i18n = useI18n()
const t = i18n.t
watch(locale, locale => i18n.locale.value = locale, { immediate: true })
// init order fields
const context = useOrderCardContext()
const phone = useField(context, 'customer.phone')
context.initialize()
</script>
<i18n locale="en-GB">
{
"callOn": "Let's call on ",
"callMade": "Call made"
}
</i18n>
<i18n locale="es-ES">
{
"callOn": "Vamos a llamar a ",
"callMade": "Llamada realizada"
}
</i18n>
<i18n locale="ru-RU">
{
"callOn": "Звоним на ",
"callMade": "Звонок совершен"
}
</i18n>
Во Vue-компоненте важно сразу предусмотреть переводы на 3 языка: русский, английский и испанский.
Далее необходимо инициализировать приложение в точках встраивания. Для новых JS-модулей используется runEndpoint(...) из @retailcrm/embed-ui-v1-endpoint/remote.
Пример инициализации виджета:
// cases/phoneReactive/index.ts
import {
defineRunner,
defineWidgetRunner,
runEndpoint,
} from '@retailcrm/embed-ui-v1-endpoint/remote'
import PhoneReactiveExtension from './PhoneReactiveExtension.vue'
runEndpoint(defineRunner({
widgets: [{
'order/card:delivery.address': defineWidgetRunner(PhoneReactiveExtension),
}],
}))
В этом сценарии frontend-часть модуля регистрируется как JS-entrypoint. В дескрипторе frontend-части для такого модуля необходимо указать runner: "worker".
Если модуль содержит несколько виджетов, их можно описать в одном endpoint:
// cases/complexModule/index.ts
import {
defineRunner,
defineWidgetRunner,
runEndpoint,
} from '@retailcrm/embed-ui-v1-endpoint/remote'
import DeliveryAddressWidget from './DeliveryAddressWidget.vue'
import CustomerPhoneWidget from './CustomerPhoneWidget.vue'
runEndpoint(defineRunner({
widgets: [
{
'order/card:delivery.address': defineWidgetRunner(DeliveryAddressWidget),
},
{
'order/card:customer.phone': defineWidgetRunner(CustomerPhoneWidget),
},
],
}))
Для комплексных модулей со страницами и/или несколькими виджетами также используется runEndpoint(...) из @retailcrm/embed-ui-v1-endpoint/remote вместе с:
defineWidgetRunner(...)— для виджетов;definePageRunner(...)— для страниц;defineRunner(...)— если модуль объединяет и виджеты, и страницы.
Такой endpoint должен публиковаться как JS-entrypoint и запускаться с runner: "worker".
Пример единой точки входа для виджетов и страниц приведен в статье «Создание собственных страниц JS-модулей».
Примечание
Пример ниже относится к legacy-сценарию
iframe. Для новых модулей рекомендуется использовать@retailcrm/embed-ui-v1-endpoint/remoteиrunner: "worker".
В legacy-сценарии приложение инициализируется через createWidgetEndpoint(...), а точка входа публикуется как HTML-страница. Такой вариант не рекомендуется использовать для новых модулей.
// cases/phoneReactive/index.ts
import { createWidgetEndpoint } from '@retailcrm/embed-ui'
import { fromInsideIframe } from '@remote-ui/rpc'
import { createI18n } from 'vue-i18n'
import PhoneReactiveExtension from './PhoneReactiveExtension.vue'
createWidgetEndpoint({
async run (createApp, root, pinia) {
const i18n = createI18n({ legacy: false, fallbackLocale: 'en-GB' })
const app = createApp(PhoneReactiveExtension)
app.use(pinia)
app.use(i18n)
app.mount(root)
return () => app.unmount()
},
}, fromInsideIframe())
Примеры расширений вы можете посмотреть в библиотеке примеров @retailcrm/core-ui-extensions-examples.
Также могут быть полезны для изучения @omnicajs/vue-remote и @retailcrm/embed-ui.
Возможности и ограничения JS-модулей
1. Ограничения на значения атрибутов элементов в шаблонах Vue-компонентов
Элементам можно присваивать атрибуты, например:
<div
:class="someClass"
:style="someStyle"
:data-value="someValue"
/>
В примере выше атрибуты — это class, style и data-value. Нужно помнить, что в эти атрибуты можно передавать только простые стандартные данные: string, number, boolean, null, undefined. Также могут быть переданы массивы простых данных, объекты ключ-значение, в которых значения — простые данные, либо комбинации перечисленных типов. Любые другие данные, переданные как атрибуты, приведут к сбою работы виджета.
Например, нельзя передать экземпляр такого класса:
class MyClass {
toString() {
return somethingThatIsString
}
}
и ожидать, что произойдет автоматическая конвертация. Вместо этого произойдет сбой.
Атрибут ref из Vue на native-элементах поддерживается как remote template ref при настройке tooling:
<div ref="myRef" />
Для корректной типизации нужно подключить плагин @omnicajs/vue-remote/tooling в настройках Vue compiler:
{
"vueCompilerOptions": {
"plugins": [
"@omnicajs/vue-remote/tooling"
]
}
}
Важно учитывать, что такой ref указывает на proxy-узел remote-дерева. Он не является HTMLElement, поэтому DOM API вроде getBoundingClientRect, classList или прямого querySelector через него недоступны.
2. Выполнение HTTP-запросов
В JS-приложении можно выполнять HTTP-запросы только через JS API. Другие способы выполнения HTTP-запросов использовать запрещено, они не будут работать.
Важно!
Вы можете найти работу с http-вызовами в примере
cases/fiscalReceiptsбиблиотеки примеров @retailcrm/core-ui-extensions-examples.
Нужно инициализировать переменную от useHost() функции, у которой есть метод httpCall() для http-вызовов.
import { useHost } from '@retailcrm/embed-ui'
const host = useHost()
const { body, status } = await host.httpCall('/get-dictionary')
if (status === 200) {
const response = JSON.parse(body) // или другой способ обработки, если ожидается не-JSON ответ
} else {
throw new Error(`HTTP request failed with status: ${status}, body: ${body}`)
}
У функции httpCall(path: string, payload?: string|object) 2 параметра:
- Обязательный параметр
path, на который в бекенд модуля будет отправлен запрос - Опциональный параметр
payload, в котором строкой или json-объектом можно передать дополнительные параметры
Примеры вызовов:
host.httpCall('/get-dictionary')host.httpCall('/get-dictionary', 'somePayload')host.httpCall('/get-dictionary', { order_id: orderId })
При вызове функции система отправляет на {baseUrl}/{path} бекенда модуля POST-запрос c Content-Type: application/x-www-form-urlencoded'.
Например, если модуль задал baseUrl, равный https://some-module.tech и указал path, равный /get-dictionary, то запрос придет на https://some-module.tech/get-dictionary.
В запросе может прийти 2 form-encoded параметра:
clientId=<clientIdForAccount>— обязательный параметр со значениемclientId, которое модуль установил в данном аккаунте системыpayload=<payload>— опциональный параметр, если был переданpayload
Если payload был указан строкой, то он будет передан как есть. Если был указан json-объект, то в payload будет передано его строковое представление
Пример передаваемых form-encoded данных:
clientId=client-id-xxx
payload={"order_id":85}
В ответ нужно вернуть ответ с нужным HTTP-статусом и телом. Они будут возвращены в ответе функции httpCall() как есть. В разборе ответа стоит проверять статус и обрабатывать ожидаемый и неожидаемый статус ответа.
Если в ответе возвращается JSON, то в JS-модуле его нужно самостоятельно распарсить.
Система отводит 2 секунды на установку соединения и 5 секунд на формирование ответа. Если превышен таймаут, то вернется HTTP-статус 500 с соответствующей ошибкой завершения по таймауту.
Важно!
При инициализации JS-модуля прямо на странице системы через
window['CRM'].embed.register()HTTP-вызовы, содержащиеся в его логике, будут возвращать HTTP-статус 503 c телом ответаREQUIRED_INTEGRATION_MODULE.
Более подробный пример с http-вызовами
import {
useOrderCardContext,
useHost,
useField,
} from '@retailcrm/embed-ui'
import { ref } from 'vue'
const order = useOrderCardContext()
const orderId = useField(order, 'id')
order.initialize()
const host = useHost()
const fetchDictionary = async () => {
const { body, status } = await host.httpCall('/get-dictionary', { order_id: orderId })
if (status === 200) {
return JSON.parse(body) // или другой способ обработки, если ожидается не-JSON ответ
}
throw new Error(`HTTP request failed with status: ${status}, body: ${body}`)
}
// можно при взаимодействии с интерфейсом вызвать
const makeSomething = async (data) => {
const { body, status } = await host.httpCall('/make-something', data)
if (status !== 200) {
throw new Error(`HTTP request failed with status: ${status}, body: ${body}`)
}
// опционально, обработка ответа
}
const dictionary = ref([])
fetchDictionary().then(data => dictionary.value = data)
3. Навигация и ссылки
Для генерации ссылок и переходов по страницам системы используются:
useRouter()— для генерации URL;useHost().goTo(...)— для переходов;getLocation(),replaceQuery()иpushQuery()— для страниц модуля.
Подробно это описано в статье «Навигация и маршрутизация в JS-модулях».
4. Работа с пользовательскими полями
JS-приложение может взаимодействовать с пользовательскими полями тех сущностей, в которых они поддерживаются (но на данный момент это только заказ order в таргетах order/card:*).
Важно!
Работу с пользовательскими полями можно посмотреть в примере модуля «Запись к специалисту» @retailcrm/specialist-booking.
Также вы можете найти работу с пользовательскими полями в примере
cases/customFieldViewerбиблиотеки примеров @retailcrm/core-ui-extensions-examples.
В JS API существует специальный тип реактивного контекста, который используется для взаимодействия с пользовательскими полями, определенными в аккаунте.
Для получения схемы контекста нужно использовать функцию useContext:
import { useContext } from '@retailcrm/embed-ui-v1-contexts/remote/custom'
const custom = useContext('order')
custom.initialize()
В функции нужно передать символьный код сущности. В перечне точек встраивания перечислены символьные коды сущностей с доступными пользовательскими полями в колонке «Коды пользовательских контекстов».
Далее из контекста можно получить поля и работать с ними:
Для получения пользовательского контекста используется composable-утилита useContext. В аргументе необходимо передать символьный код контекста. В примере ниже используется контекст заказа с символьным кодом order.
Для работы с конкретным полем пользовательского контекста используется useCustomField. В утилиту передаются контекст и код поля.
import { useContext } from '@retailcrm/embed-ui-v1-contexts/remote/custom'
import { useCustomField } from '@retailcrm/embed-ui'
const custom = useContext('order')
await custom.initialize()
const someField = useCustomField(custom, 'code')
const viewedAt = useCustomField(custom, 'viewedAt', { kind: 'datetime' })
someField.value = 'some-string'
viewedAt.value = new Date().toISOString()
Метод initialize() инициализирует контекст и возвращает Promise<CustomContextSchema>. После инициализации схема контекста также доступна в хранилище:
custom.schema
В initialize() можно передать обработчик ошибки с сигнатурой (rejection: Rejection) => void. Ошибка может возникнуть, если указанного контекста не существует.
Определение типа Rejection доступно в типах пакета @retailcrm/embed-ui.
custom.initialize((rejection) => {
console.error(rejection)
})
Если поле с указанным кодом не существует или контекст ещё не инициализирован, значение поля будет равно null.
По умолчанию useCustomField возвращает WritableComputedRef. При этом возможность записи значения не гарантируется. Ошибка возникнет в следующих случаях:
- в настройках поля, полученных из CRM, указан флаг
readonlyсо значениемtrue; - выполняется попытка записи в поле до инициализации контекста.
Для поля можно указать ожидаемый тип значения. Это позволяет TypeScript корректно подсвечивать дальнейшую работу с полем.
const viewedAt = useCustomField(custom, 'viewedAt', { kind: 'datetime' })
Список доступных значений для параметра kind приведён в типах пакета @retailcrm/embed-ui.
Если тип поля не совпадает со значением, указанным в параметре kind, значение поля будет равно null.
В третьем аргументе useCustomField также можно передать дополнительные опции:
readonly— логический флаг, который указывает, что поле доступно только для чтения. Если указано значениеtrue, вместоWritableComputedRefвозвращаетсяComputedRef;onReject— обработчик отклонения попытки записи в поле на стороне системы.
const viewedAt = useCustomField(custom, 'viewedAt', {
kind: 'datetime',
readonly: true,
onReject: (rejection) => {
console.error(rejection)
},
})
Для полей dictionary и multiselect_dictionary можно загрузить список значений, используя утилиту useDictionary, которая предоставляет клиент для запроса списка значений справочника.
import type { CustomDictionary } from '@retailcrm/embed-ui-v1-types/context'
import { useContext } from '@retailcrm/embed-ui-v1-contexts/remote/custom'
import { useCustomField } from '@retailcrm/embed-ui'
import { useDictionary } from '@retailcrm/embed-ui-v1-contexts/remote/custom'
const custom = useContext('order')
const dictionary = useDictionary()
const options = ref<CustomDictionary>([])
const loaded = ref(false)
custom.initialize().then(schema => {
const descriptor = schema?.fields.find(f => f.code === 'accessory')
if (descriptor && 'dictionaryCode' in descriptor) {
dictionary.query(descriptor.dictionaryCode).then((result) => {
options.value = result
loaded.value = true
})
} else {
throw new Error('No dictionary for field with code ' + props.code)
}
})
const accessory = useCustomField(custom, 'accessory', { kind: 'dictionary' })
5. Работа с действиями
Важно!
Функциональность действий доступна в @retailcrm/embed-ui с версии
v0.8.0.
Для некоторых целей встраивания определен специальный объект действий, который позволяет выполнять более сложные манипуляции с сущностями на странице.
В частности, для списка товарных позиций определен объект с идентификатором order/card, который позволяет работать с данными товарных позиций, а также добавлять или удалять позиции из заказа:
import { useActions } from '@retailcrm/embed-ui-v1-contexts/remote/order/card'
const orderActions = useActions()
orderActions.createItem({
productId: %идентификатор товара или услуги%,
offerId: %идентификатор торгового предложения%,
priceAmount: %цена в валюте заказа%,
priceTypeCode: %символьный код типа цены%,
quantity: 1,
})
Все действия являются асинхронными, т.е. возвращают Promise. В некоторых случаях Promise может возвращать значение, которое может быть использовано далее:
const index = await orderActions.createItem({
productId,
offerId,
priceAmount,
priceCode,
quantity: 1,
})
await orderActions.changeItemDiscount(index, {
amount: 0,
percent: 10,
})
Важно!
Объект действий доступен не для любых целей встраивания. Если расширение может быть запущено в нескольких разных целях, необходимо проверять, какая текущая цель используется.
Для новых JS-модулей рекомендуется использовать runEndpoint(...) из @retailcrm/embed-ui-v1-endpoint/remote. В этом сценарии текущая цель встраивания передается в компонент как prop target.
Пример инициализации виджета:
// cases/orderActions/index.ts
import {
defineRunner,
defineWidgetRunner,
runEndpoint,
} from '@retailcrm/embed-ui-v1-endpoint/remote'
import { createI18n } from 'vue-i18n'
import { useContext as useOrder } from '@retailcrm/embed-ui-v1-contexts/remote/order/card'
import { useContext as useSettings } from '@retailcrm/embed-ui-v1-contexts/remote/settings'
import { useContext as useUser } from '@retailcrm/embed-ui-v1-contexts/remote/user/current'
import WidgetApp from './WidgetApp.vue'
const beforeMount = async (app) => {
app.use(createI18n({
fallbackLocale: 'en-GB',
legacy: false,
}))
await Promise.allSettled([
useOrder(),
useSettings(),
useUser(),
].map(context => context.initialize()))
}
runEndpoint(defineRunner({
widgets: [{
'order/card:list.before': defineWidgetRunner(WidgetApp, beforeMount),
'order/card:list.after': defineWidgetRunner(WidgetApp, beforeMount),
}],
}))
В дескрипторе frontend-части для такого сценария необходимо указать runner: "worker".
И далее в WidgetApp:
<script lang="ts" setup>
import type { TargetName } from '@retailcrm/embed-ui-v1-endpoint/common'
import { useActions } from '@retailcrm/embed-ui-v1-contexts/remote/order/card'
const props = defineProps<{
target: TargetName
}>()
const orderActions = [
'order/card:list.before',
'order/card:list.after',
].includes(props.target) ? useActions() : null
// Используем optional chaining,
// так как объект действий доступен не для любых целей встраивания.
orderActions?.createItem(...)
</script>
Примечание
Пример ниже относится к legacy-сценарию
iframe. Для новых модулей рекомендуется использовать@retailcrm/embed-ui-v1-endpoint/remoteиrunner: "worker".
В legacy-сценарии приложение инициализируется через createWidgetEndpoint(...), а точка входа публикуется как HTML-страница. Такой вариант не рекомендуется использовать для новых модулей, но может потребоваться при поддержке уже существующих iframe-модулей.
// cases/orderActions/index.ts
import WidgetApp from './WidgetApp.vue'
import { createI18n } from 'vue-i18n'
import { createWidgetEndpoint } from '@retailcrm/embed-ui'
import { fromInsideIframe } from '@remote-ui/rpc'
import { useContext as useOrder } from '@retailcrm/embed-ui-v1-contexts/remote/order/card'
import { useContext as useSettings } from '@retailcrm/embed-ui-v1-contexts/remote/settings'
import { useContext as useUser } from '@retailcrm/embed-ui-v1-contexts/remote/user/current'
createWidgetEndpoint({
async run (createApp, root, pinia, target) {
const app = createApp(WidgetApp, { target }) // передадим цель в Vue-приложение
app.use(pinia)
app.use(createI18n({
fallbackLocale: 'en-GB',
legacy: false,
}))
await Promise.allSettled([
useOrder(),
useSettings(),
useUser(),
].map(context => context.initialize()))
app.mount(root)
return () => app.unmount()
},
}, fromInsideIframe())
В дескрипторе frontend-части для такого сценария указывается runner: "iframe". Значение iframe относится к legacy-режиму и не рекомендуется для новых модулей.
Если для разных целей встраивания требуется разная логика или разные компоненты, виджеты можно описать отдельно:
import {
defineRunner,
defineWidgetRunner,
runEndpoint,
} from '@retailcrm/embed-ui-v1-endpoint/remote'
import BeforeListWidget from './BeforeListWidget.vue'
import AfterListWidget from './AfterListWidget.vue'
runEndpoint(defineRunner({
widgets: [{
'order/card:list.before': defineWidgetRunner(BeforeListWidget),
'order/card:list.after': defineWidgetRunner(AfterListWidget),
}],
}))
Такой вариант снижает количество проверок внутри компонента и подходит для случаев, когда поведение виджета заметно отличается в разных целях встраивания.
Примечание
Сценарий с
createWidgetEndpoint(...)иfromInsideIframe()относится к legacy-режимуiframe. Для новых модулей рекомендуется использовать@retailcrm/embed-ui-v1-endpoint/remoteиrunner: "worker".
6. Работа с ссылками на страницы системы
В JS API есть возможность генерировать ссылки на страницы системы и перенаправлять пользователя на заданные страницы системы.
Страницы системы, с которыми можно работать в JS API, перечислены в справочнике роутов.
Для генерации требуется добавить в проект JS-модуля пакет @omnicajs/symfony-router, который предоставляет код
генерации:
yarn add @omnicajs/symfony-router
Генерация ссылок на страницы системы
Важно!
Функциональность генерации ссылок доступна в @retailcrm/embed-ui с версии
v0.5.15.Ссылки на страницы системы можно формировать только указанным ниже способом. Если вы указываете ссылки в явном виде, вы рискуете тем, что они могут стать неработоспособными в случае их изменения со стороны системы.
Ниже показан пример генерации ссылки на страницу системы. Если текущий пользователь с правами администратора, то подставляется ссылка на страницу пользователя в разделе Настройки либо ссылка на страницу менеджера в противном случае.
<template>
<UiLink v-if="isAdmin" :href="router.generate('crm_manager_show', { id: userId })">
View profile
</UiLink>
<UiLink v-else :href="router.generate('crm_users_edit', { id: userId })">
Edit profile
</UiLink>
</template>
<script lang="ts" setup>
import { useContext as useSettings } from '@retailcrm/embed-ui-v1-contexts/remote/settings'
import { useContext as useUser } from '@retailcrm/embed-ui-v1-contexts/remote/user/current'
import {
useField,
useRouter
} from '@retailcrm/embed-ui'
const user = useUser()
const userId = useField(user, 'id')
const isAdmin = useField(user, 'isAdmin')
user.initialize()
const settings = useSettings()
// Нужно инициализировать контекст настроек,
// т.к. useRouter утилита использует этот контекст и его поле `system.routing`
settings.initialize()
// useRouter возвращает ComputedRef
const router = useRouter()
</script>
Также вы можете посмотреть генерацию и подстановку ссылок в кейсе cases/orderNotes библиотеки примеров @retailcrm/core-ui-extensions-examples.
Перенаправление пользователя на определенную страницу системы
Важно!
Функциональность перенаправления пользователя доступна в @retailcrm/embed-ui с версии
v0.9.2.Перенаправлять можно только указанным ниже способом.
Ниже показан пример перенаправления с помощью функции goTo().
import { useHost } from '@retailcrm/embed-ui'
const host = useHost()
host.goTo('crm_orders') // переход на страницу списка заказов
const id = 12345
host.goTo('crm_orders_edit', { id }) // переход на страницу с формой редактирования заказа
Для сценариев со страницами также может использоваться query string. Подробно работа с API хоста, useRouter(), goTo(), getLocation() и query string описана в статье «Навигация и маршрутизация в JS-модулях».
Встраивание в интерфейс системы
Сборка JS-приложения
Вам требуется выполнить сборку приложения средствами webpack, vite и т.п. В @retailcrm/core-ui-extensions-examples сборка производится командой yarn build. В папке сборки должен появиться набор из index.html, css-файла и js-файла.
После этого требуется создать в папке сборки файл manifest.json
Структура manifest.json
Файл manifest.json содержит метаданные для JS-приложения. В @retailcrm/core-ui-extensions-examples файл создается автоматически при выполнении команды make zip-archive. Пример содержимого файла manifest.json для script-entrypoint и v1-endpoint:
{
"code": "core-ui-extensions",
"version": 154,
"targets": ["order/card:delivery.address"],
"pages": [
{
"code": "settings",
"menu": "private_main_menu",
"parentMenuItemCode": "settings",
"menuItemOrdering": 100,
"menuItemTitle": {
"ru": "Настройки",
"en": "Settings",
"es": "Configuración"
},
"pageHelpLink": null
}
],
"entrypoint": "extension.xxx.js",
"scripts": ["extension.xxx.js"],
"runner": "worker",
"stylesheet": "extension.xxx.css"
}
Где:
code: string— уникальный код приложения. Обязательный параметр.version: number|string— версия приложения. Обязательный параметр.targets: string[]— точки встраивания приложения. Нужно указать, если модуль добавляет виджеты в существующие страницы.pages: string[]|object[]— страницы модуля. Нужно указать, если модуль добавляет собственные страницы внутри CRM.entrypoint: string— файл, который является точкой входа для приложения. Для iframe-сценария это HTML-файл, дляv1-endpointобычно JS-файл.scripts: string[]— JS-файлы приложения. Обязательный параметр.runner: "iframe"|"worker"— способ запуска frontend-части. Дляv1-endpointи сценариев со страницами требуетсяworker. Значениеiframeотносится к legacy-сценарию и не рекомендуется для новых модулей. Для новых модулей, script-entrypoint,v1-endpointи собственных страниц необходимо использоватьworker.stylesheet: ?string— CSS-файл приложения. Необязательный параметр.
В актуальном API v5 для регистрации frontend-части используются поля integrationModule[integrations][embedJs][entrypoint], runner, targets, pages и stylesheet. Поле entrypointType в актуальной таблице параметров API v5 не используется.
Встраивание JS-приложения в интерфейс в ходе разработки
В ходе разработки для целей отладки вы можете встраивать JS-приложение на тех страницах, на которых находятся точки встраивания (targets) приложения.
Для этого запустите сервер, который будет отдавать файлы вашего приложения на базе nodejs, nginx или другого веб-сервера. В @retailcrm/core-ui-extensions-examples есть пример сервера в файле server.mjs. Вы можете его запустить командой node server.mjs.
После этого на нужной странице системе достаточно вызвать window['CRM'].embed.register(). Например:
window['CRM'].embed.register({
"uuid": "62aa8145-ed53-4862-b28f-f1bc6b36a3a3",
"targets": [
"order/card:delivery.address"
],
"entrypoint": "http://localhost:3000/extension/62aa8145-ed53-4862-b28f-f1bc6b36a3a3",
"stylesheet": "http://localhost:3000/extension/62aa8145-ed53-4862-b28f-f1bc6b36a3a3/stylesheet"
})
Значение uuid может быть произвольным.
Дескриптор frontend-части
В актуальном сценарии frontend-часть модуля описывается дескриптором расширения. Именно этот дескриптор определяет, как CRM должна запускать frontend-часть модуля, где она должна отображаться и какие ресурсы для нее нужно загружать.
Практически дескриптор задает:
- идентификатор frontend-части модуля;
- точку входа frontend-приложения;
- способ запуска frontend-части;
- точки встраивания для виджетов;
- страницы модуля, если они поддерживаются;
- стили frontend-части;
- связанные параметры публикации, по которым CRM получает ресурсы модуля и взаимодействует с его backend-частью.
В дескрипторе frontend-части используются следующие поля.
| Поле | Описание |
|---|---|
uuid |
Идентификатор frontend-части модуля. По нему CRM различает расширения и строит URL ресурсов frontend-части, например script- и stylesheet-ресурсов. |
entrypoint |
Точка входа frontend-части модуля. Для модулей на iframe это обычно HTML-страница, которая запускает приложение в точке встраивания. Для комплексных модулей со страницами и/или несколькими виджетами используется script-entrypoint, через который CRM поднимает remote-приложение. |
runner |
Способ запуска frontend-части. Для новых модулей, v1-endpoint и собственных страниц необходимо указывать worker. Значение iframe относится к legacy-сценарию, не рекомендуется для новых модулей и может быть убрано в будущем. |
targets |
Перечень точек встраивания, в которых CRM должна отображать виджеты модуля. |
pages |
Описание собственных страниц модуля внутри CRM. Если поле pages задано, CRM добавляет модульные страницы в навигацию и открывает их по маршрутам вида /modules/<moduleCode>/<pageCode>. |
stylesheet |
Ресурс со стилями frontend-части модуля. CRM подключает его вместе с frontend-приложением, если для расширения он задан. |
baseUrl |
Базовый адрес сервера модуля. Относительно него CRM получает frontend-ресурсы и выполняет backend-вызовы модуля. |
clientId |
Идентификатор установленного экземпляра модуля в конкретном аккаунте. Он используется при взаимодействии CRM с backend-частью модуля. |
Публикация frontend-части
Существует два способа регистрации frontend-части JS-модуля в системе, которые описаны ниже: централизованный (через Маркетплейс) и для конкретного аккаунта (через API).
Важно!
Эти два способа являются взаимоисключающими.
Если вы активировали модуль из маркетплейса, то попытка активировать модуль в конкретном аккаунте через API приведет к ошибке:
Can not create connection with inline js configuration for module with embed js supportЕсли вы планируете использовать модуль только в конкретных аккаунтах с индивидуальной конфигурацией через API (например, для частных разработок), не загружайте JS-архив в Партнерском кабинете.
Публикация в маркетплейсе
Как было описано в начале статьи, модуль заводится в маркетплейсе, как и любой другой модуль. Если модуль содержит frontend-часть, JS-функциональность необходимо включить в карточке модуля в партнерском кабинете.
После этого в карточке модуля будет доступна форма «JS-файл» для загрузки архива JS-части модуля. В @retailcrm/core-ui-extensions-examples есть команда make zip-archive, где можно посмотреть, как создать архив с модулем.
Репозиторий @retailcrm/core-ui-extensions-examples также демонстрирует способы регистрации JS-модуля через API CRM. Для сценария со страницами можно посмотреть кейс cases/promoModule и команду:
make publish-case case=promoModule
Эта команда собирает архив, читает дескриптор из extensionrc.json и отправляет конфигурацию frontend-части в API CRM.
Архив загружается через форму. Если модуль опубликован в Маркетплейсе, то загруженная версия начнет инициализироваться в интерфейсе аккаунтов системы, где включен модуль.
При изменении исходного кода модуля (добавлении новых функций или исправлении ошибок) вам требуется его собрать, создать архив с новым кодом и загрузить через форму. Новая версия будет практически сразу доступна в аккаунтах, где включен модуль (возможны задержки до 10 мин).
Архив с новой версией JS-части модуля можно загрузить через API партнерского кабинета. Это позволяет настроить CI-деплой новой версии.
| Параметр | Значение |
|---|---|
| Метод и URL | POST https://account.retailcrm.ru/api/public/v1/marketplace/module/{moduleCode}/js/upload |
| Параметр пути | moduleCode — код модуля в маркетплейсе |
| Заголовок | X-MODULE-UPLOAD-TOKEN: <token> |
| Тип тела запроса | multipart/form-data |
| Поле файла | new_version — ZIP-архив с новой версией JS-модуля, не более 5 МБ |
Пример запроса:
curl -X POST "https://account.retailcrm.ru/api/public/v1/marketplace/module/<moduleCode>/js/upload" \
-H "X-MODULE-UPLOAD-TOKEN: <token>" \
-F "new_version=@build/module.zip"
При успешной загрузке архив будет принят, а API вернёт ответ с кодом 200 OK.
Пример успешного ответа:
{
"success": true,
"moduleCode": "<moduleCode>"
}
Обратите внимание, что обработка новой версии асинхронная, и результат обработки будет виден в личном кабинете.
Если модуль публикуется централизованно, должны быть согласованы:
- карточка интеграционного модуля;
- backend-часть модуля;
- дескриптор frontend-части;
- ресурсы frontend-части;
targetsи, при наличии,pages.
Регистрация JS-модуля в отдельно взятом аккаунте
Кастомизированный JS-модуль для отдельного аккаунта системы можно зарегистрировать с помощью API-метода POST /api/v5/integration-modules/{code}/edit.
| Параметр | Описание |
|---|---|
integrationModule[baseUrl] |
Базовый адрес сервера модуля |
integrationModule[integrations][embedJs][entrypoint] |
Относительный путь к JS-файлу, который является точкой входа frontend-части. Для legacy-сценария iframe указывается путь к HTML-файлу. |
integrationModule[integrations][embedJs][runner] |
Способ запуска frontend-части: worker для актуального сценария или iframe для legacy-сценария. |
integrationModule[integrations][embedJs][stylesheet] |
Относительный путь к CSS-стилям |
integrationModule[integrations][embedJs][targets] |
Массив точек встраивания |
integrationModule[integrations][embedJs][pages][] |
Массив страниц, если модуль добавляет собственные страницы |
Если вызов метода выполнен успешно, JS-часть модуля будет инициализироваться на страницах целевого аккаунта, где находятся указанные точки встраивания.
Важно!
Если модуль публикует собственные страницы, необходимо использовать конфигурацию
pages,runner: "worker"и сценарий выполнения через@retailcrm/embed-ui-v1-endpoint/remote. Подробнее см. в статье Создание собственных страниц JS-модулей.