JS-модули
Скопировать ссылку на статью
Скопировано

Содержание

JS-модули позволяют расширить возможности интерфейса системы.

С помощью frontend-части модуль может добавлять элементы в существующие разделы CRM, открывать собственные страницы и взаимодействовать с интерфейсом через API хоста.

Архитектура JS-модуля

JS-модули является расширенной версией стандартных модулей Маркетплейса. Отличием является то, что они состоят из двух частей: backend-часть и frontend-часть.

Важно!

На Github представлен полноценный пример реализации модуля с backend-частью на Symfony @retailcrm/specialist-booking.

Также доступна библиотека примеров @retailcrm/core-ui-extensions-examples.

Backend-часть JS-модуля

Backend-часть JS-модуля разрабатывается также, как и у стандартных модулей. В ее подготовке вам помогут статьи про основные требования к модулям, подключение и активацию модуля и добавление модуля в Маркетплейс.

Backend-часть модуля должна как минимум содержать логику по подключению и активации модуля в Маркетплейсе.

Схема работы backend-части JS-модуля

Если frontend-часть модуля будет обращаться к backend-части, то последняя помимо базовой логики должна также предоставлять ендпоинты для обработки запросов, отправляемых из frontend-части.

Схема взаимодействия frontend-части JS-модуля с backend-частью

Важно!

После подготовки 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-модулей.

Благодарим за отзыв.
Была ли статья полезна?
Нет
  • Рекомендации не помогли
  • Нет ответа на мой вопрос
  • Текст трудно понять
  • Не нравится описанный функционал
Да
Предыдущая статья
Работа с ценами на модуль
В Маркетплейс можно размещать как бесплатные, так и платные модули.
Следующая статья
Точки встраивания JS-модулей
Точки интерфейса, куда могут встроиться JS-модули, а также доступные в этих точках объекты