Desarrollo de un widget simple
En este tutorial, explicaremos cómo crear un widget básico que se puede utilizar para una integración privada.
Los objetivos principales de este tutorial son:
- proporcionar una comprensión profunda de cómo funciona un widget.
- mostrar la comunicación entre diferentes archivos dentro del widget
¿Qué es un widget?
Un widget es una pequeña aplicación o herramienta funcional que se puede integrar en la plataforma Kommo para ampliar sus capacidades y mejorar su funcionalidad.
Los widgets en Kommo se utilizan para proporcionar funcionalidades adicionales, agilizar los flujos de trabajo y mejorar la experiencia del usuario al permitir una interacción fluida con servicios de terceros o funcionalidades personalizadas directamente dentro de la interfaz del CRM.

Requisitos previos
Antes de comenzar a trabajar en un widget, hay varios aspectos que necesitas conocer o explorar.
1. Cuenta de Kommo
Para crear una integración y cargar un widget, necesitas registrar una cuenta en Kommo. Puedes utilizar cualquier tipo de cuenta (de prueba/técnica/empresarial) para cargar y probar tu widget, pero recomendamos usar una cuenta de prueba para probar tu integración.
2. Usuario confiable de Kommo
Para desarrollar para los usuarios de Kommo, deberías ser un usuario experimentado de Kommo. Se necesita algo de tiempo para entender cómo funciona el sistema y cómo interactúan entre sí los diferentes objetos. Si lo único que sabes sobre Kommo es su nombre, te recomendamos encarecidamente dedicar tiempo a explorarlo. Esto te ayudará a comprender mejor las necesidades de los futuros usuarios de tu widget.
3. Use VS Code
Debes estar familiarizado con el uso de VS Code u otro editor de código. Los desarrolladores pueden usar VS Code para construir, editar y depurar el código, y luego pueden publicar fácilmente la aplicación que están desarrollando.
4. Conocimiento básico de CSS y HTML
Se espera que tengas algunos conocimientos de CSS y HTML, ya que un widget se centra en la parte visual de una integración. Ambos son fundamentales para el desarrollo web, siendo HTML el que proporciona la estructura básica y CSS el que agrega los elementos de estilo.
Nuestro objetivo no es enseñarte a programar.
5. Conocimientos sólidos de JavaScript
El archivo script.js se encuentra escrito en JavaScript.
Para desarrollar un widget, deberías tener un buen conocimiento de JavaScript.
Necesitas estar familiarizado con conceptos de JavaScript como variables, funciones, objetos, arreglos y manipulación del DOM.
➕ Además, el conocimiento de manejo de eventos, programación asincrónica y AJAX será beneficioso para crear widgets interactivos y dinámicos.
6. Familiaridad con jQuery
JQuery es una biblioteca de JavaScript que fue creada para facilitar tu trabajo.
$(document).ready(function(){
$("p").click(function(){
$(this).hide();
});
});
7. Conocimiento de Twig
Asegúrate de estar familiarizado con Twig. Twig es un motor de plantillas diseñado para generar HTML. Se utiliza principalmente con PHP, pero nosotros lo utilizamos junto con JavaScript para generar HTML del lado del cliente en lugar de hacerlo en el servidor.
Un ejemplo de delete_button.twig:
<span id="{{ id }}" class="button-delete {{ class_name }}">
<span class="icon icon-delete-trash"></span>
{{text}}
</span>
Estructura del widget
En Kommo, un widget se parece a una carpeta de archivos.
Algunos de los archivos son obligatorios, mientras que otros añaden funcionalidad y diseño.

Echemos un vistazo más de cerca a cada componente del widget.
manifest.json
manifest.json es el archivo principal de un widget. Este enlaza los archivos de localización, las imágenes y los archivos con JS.
El archivo en sí incluye el nombre del widget, la descripción, las imágenes, la versión, los archivos de idioma y diferentes tipos de configuraciones.
En manifest.json debes especificar en qué Marketplace se mostrará tu widget (aquí debes elegir los idiomas), qué parte del sistema inicializará tu widget (por ej.: el perfil de contacto o el pipeline digital), puedes proporcionar un correo electrónico para consultas de los clientes y también mostrar la versión del widget.
script.js
Un widget se representa como un objeto, que contiene propiedades y métodos útiles para trabajar con él. Cuando el sistema carga los widgets, añade la funcionalidad descrita en script.js to the existing system Widget object. al objeto Widget existente en el sistema. Esto significa que el objeto CustomWidget
hereda propiedades y métodos que serán útiles para el trabajo posterior.
La porción de JavaScript (JS) del widget contiene los componentes requeridos y funciones adicionales. También incluye funciones de callback que se activan bajo condiciones específicas.
Con el script.js tienes una herramienta simple para interactuar con el DOM y realizar solicitudes entre dominios. Puedes utilizarla para crear widgets de texto básicos, modificar el diseño de los elementos de la página, generar bloques de información a partir de datos externos o enviar datos a servicios externos. Estos cambios funcionarán de inmediato para todos los usuarios de tu cuenta.
i18n
i18n es la carpeta donde añades los archivos de localización.
Si deseas que tu widget esté disponible en todos los idiomas, debes añadir tres archivos de localización: en.json, es.json, pt.json.
Si tienes más de una localización, ten en cuenta que deben tener la misma estructura.
Ejemplo
{
"widget":{
"name":"My Widget",
"short_description":"Short description",
"description":"Full description",
"tour_description":"Tour description"
}
}
{
"widget":{
"name":"Mi Widget",
"short_description":"Descripción breve",
"description":"Descripción completa",
"tour_description":"Descripción del Tour"
}
}
{
"widget":{
"name":"Meu widget",
"short_description":"Descrição curta",
"description":"Descrição completa",
"tour_description":"Descrição de tour"
}
}
templates
Puedes pasar una de las plantillas de nuestro sistema a la función; para ello, necesitas especificar un enlace a la plantilla en el objeto de datos pasado: ref: ‘/tmpl/controls/#TEMPLATE_NAME#.twig’
, donde #TEMPLATE_NAME#
es una de las plantillas del sistema.
Pasas ref: ‘/tmpl/controls/#TEMPLATE_NAME#.twig
y los parámetros de la plantilla al método self.render()
.
De esta manera, no necesitas añadir las plantillas a la carpeta templates.
define(['jquery'], function ($) {
'use strict';
return function () {
var self = this;
this.callbacks = {
render: function () {
return true;
},
init: function () {
return true;
},
bind_actions: function () {
return true;
},
settings: function () {
var $modal_body = $('.modal.' + self.get_settings().widget_code + ' .modal-body'),
$widget_settings = $modal_body.find('.widget_settings_block');
$widget_settings.html(
self.render({ ref: '/tmpl/controls/button.twig' }, {
text: 'Botón'
})
);
return true;
},
onSave: function () {
return true;
}
};
};
});
Un botón del ejemplo anterior se verá así

Puedes pasar otros parámetros además de text
.
Por ejemplo, si pasas true
al parámetro blue
, el botón se volverá azul.

Puedes proporcionar referencias no solo a plantillas del sistema existentes sino también a tus propias plantillas. Para hacerlo, necesitarás crear una carpeta templates dentro de la carpeta de tu widget y colocar la plantilla twig en ella.
images
Incluso si tu widget no se muestra explícitamente en ningún lugar, excepto en la sección de integración, aún debes cargar la carpeta images.
Número de logos que deben cargarse: 5
🌠logo_min.png
🎑 logo_mediano.png
🌁logo_principal.png
🏞logo.png
🌇logo_pequeño.png
Si tu widget se inicializa en el Pipeline Digital, también debes añadir logo_dp.png a la carpeta images.
El formato PNG es el único que se puede utilizar para los logos.
Otras imágenes (excepto las imágenes de la presentación de diapositivas) pueden estar en formato SVG o JPG.
style.css
El archivo CSS contiene los estilos para el widget.
Para garantizar que el widget no entre en conflicto con otros elementos y widgets del sistema, su archivo CSS debe contener nombres de clases únicos para todos los elementos principales y elementos hijos.
Limitaciones
- Si creas un widget público, estará disponible para todos en el Marketplace (en el idioma correspondiente), pero si creas uno privado deberás instalarlo en cada cuenta en la que se supone que se va a utilizar.
- Un widget privado no se inicializa en la barra de menú izquierda.
Caso de prueba
Cuando estábamos planificando este tutorial, queríamos algo que mostrara la interacción entre diferentes campos (incluidos los campos personalizados), que utilizara variables de entorno, algo que permitiera añadir plantillas, que mostrara cómo realizar un clic cuando un usuario haga clic en otro lugar del sistema, y que otorgara diferentes derechos a los usuarios
Así que se nos ocurrió la idea de un widget de cumpleaños.
Qué hace el widget en detalle
En la configuración del widget, puedes hacer lo siguiente:
- Elegir un perfil (Contacto o Lead) con un campo personalizado de Cumpleaños.
- Si no tienes este campo, puedes crearlo en la configuración.
- Elige un perfil donde se creará una tarea, el tipo de tarea, los usuarios responsables de la tarea y el texto de la tarea.
- Crea una plantilla que pueda ser enviada a un cliente
Si es el día del cumpleaños del cliente y el usuario responsable de la tarea accede al perfil del cliente, se creará una tarea. Si otro usuario responsable o el mismo usuario vuelve a ingresar al perfil, no se creará la tarea nuevamente.
El widget se mostrará en el lado derecho del panel de un perfil de lead. Si la fecha actual coincide con el cumpleaños establecido en el perfil del lead o contacto, verás un botón Enviar un deseo de cumpleaños. Si no coincide, solo verás un mensaje que indica que ninguno de tus clientes celebra su cumpleaños ese día.
Cuando hagas clic en el botón, se abrirá una lista de plantillas, con las plantillas del widget en la parte superior, para que puedas elegir una de ellas y enviársela a tu cliente.
Caso de prueba del widget

Vamos directamente a la carpeta con las partes del widget.
manifest.json
Echemos un vistazo al esqueleto del widget y su parte principal, manifest.json.
{
"widget": {
"name": "widget.name",
"description": "widget.description",
"short_description": "widget.short_description",
"version": "1.0.1",
"interface_version": 2,
"init_once": false,
"locale": [
"en",
"pt",
"es"
],
"installation": true,
"support": {
"link": "https://kommo.com",
"email": "[email protected]"
}
},
"locations": [
"settings",
"lcard-1",
"ccard-0"
],
"tour": {
"is_tour": true,
"tour_images": {
"en": [
"/images/slideshow_1_en.jpg",
"/images/slideshow_2_en.jpg",
"/images/slideshow_3_en.jpg"
],
"es": [
"/images/slideshow_1_es.jpg",
"/images/slideshow_2_es.jpg",
"/images/slideshow_3_es.jpg"
],
"pt": [
"/images/slideshow_1_pt.jpg",
"/images/slideshow_2_pt.jpg",
"/images/slideshow_3_pt.jpg"
]
},
"tour_description": "widget.tour_description"
},
"settings": {
"custom": {
"name": "settings.custom",
"type": "custom",
"required": false
}
}
}
Examinaremos más de cerca cada parte del widget.
name y description
"widget": {
"name": "widget.name",
"description": "widget.description",
"short_description": "widget.short_description",
Aquí mostramos que el nombre del widget, la descripción y una breve descripción se tomarán de los archivos de localización.
version y init_once
"version": "1.0.1",
"interface_version": 2,
"init_once": false,
Aquí especificas la versión de tu widget, pero como no necesitas actualizar un widget privado, puedes pasar cualquier versión.
"interface_version"
debe estar siempre configurado en 2
.
Dado que el widget en el que trabajamos no es VoIP, configuramos"init_once"
en false
.
El método init_once
es responsable de la inicialización del widget. Si no hay un contexto común para todas las páginas, deberías pasar false
.
idiomas
"locale": [
"en",
"pt",
"es"
],
Dado que creamos un widget que se puede ver al cambiar de idioma, mencionamos todos los idiomas que admitimos en el arreglo "locale"
.
"en"
representa el Inglés.
"pt"
representa el Portugués.
"es"
representa el Español.
installation y support
"installation": true,
"support": {
"link": "https://kommo.com",
"email": "[email protected]"
}
},
Configuramos "installation"
en true
ya que necesitamos configurar los ajustes. Normalmente, configuramos "installation"
en false
si los ajustes deben ser gestionados en otro sistema que interactúa con Kommo a través de la API.
locations
"locations": [
"settings",
"lcard-1",
"ccard-0"
],
El campo "locations"
define en qué áreas se inicializará (y se verá) el widget.
Nuestro widget se inicializará en:
"settings"
(página de instalación y configuración del widget)."lcard-1"
(el widget se mostrará en el lado derecho del panel del perfil de un Lead)."ccard-0"
(el widget se iniciará en el perfil de un Contacto, pero no aparecerá en el panel derecho).
Presentación de diapositivas del widget
"tour": {
"is_tour": true,
"tour_images": {
"en": ["/images/slideshow_1_en.jpg", "/images/slideshow_2_en.jpg", "/images/slideshow_3_en.jpg"],
"es": ["/images/slideshow_1_es.jpg", "/images/slideshow_2_es.jpg", "/images/slideshow_3_es.jpg"],
"pt": ["/images/slideshow_1_pt.jpg", "/images/slideshow_2_pt.jpg", "/images/slideshow_3_pt.jpg"]
},
"tour_description": "widget.tour_description"
}
Proporcionamos tres imágenes en cada idioma para la presentación de diapositivas del widget.
"widget.tour_description"
se obtendrá de los archivos de i18n.
settings
"settings": {
"custom": {
"name": "settings.custom",
"type": "custom",
"required": false
}
}
}
Dado que tenemos diferentes tipos de entradas, creamos configuraciones personalizadas. Todos los campos se obtendrán de los archivos de i18n.
images
Debemos añadir todas las imágenes que el widget utilizará a la carpeta images. images.
Hay muchas imágenes en la carpeta. Estas son útiles para diferentes propósitos.
logos
Los logos son imágenes que se muestran en las áreas de inicialización del widget. Todos deben ser añadidos a la carpeta, de lo contrario, recibirás un mensaje de error del sistema como el siguiente:

Las imágenes más importantes que debemos añadir son:

logo_principal.png 400px x 272px

logo_mediano.png 240px x 84px

logo.png 130px x 100px

logo_pequeño.png 108px x 108px

logo_min.png 84px x 84px
¡Las imágenes de los logos deben estar en formato PNG!
Dado que el widget no se inicia en el Pipeline Digital, no añadimos un logo_dp a la carpeta images.
Imágenes de la presentación de diapositivas
Las imágenes de la presentación de diapositivas se muestran en la pantalla de instalación para mostrar tu integración en acción.
No son obligatorias para una integración privada, pero las añadimos con el fin de facilitar el tutorial.
Lo que debes recordar al añadir imágenes de la presentación de diapositivas::
- debes añadir de 1 a 5 imágenes.
- deben tener 1188×616 píxeles cada una en formato JPG.
- deben mostrar visualmente la funcionalidad de la integración, cómo se verá dentro de Kommo y su valor para el usuario.
- deben ser de alta calidad.

Si tu widget soporta diferentes idiomas, deberías proporcionar imágenes de la presentación de diapositivas en todos los idiomas.
otras imágenes
Otras imágenes están en formato SVG.
Se supone que deben mejorar la interfaz del widget haciéndola más intuitiva.
Una interfaz intuitiva es una interfaz fácil de utilizar que funciona como se espera y se siente natural para el usuario.
Es bastante evidente que, al pasar el cursor o hacer clic en este botón, se mostrará información sobre un tema que podrías tener dificultad para entender.
localización
Cada línea y palabra utilizada en el widget debe ser traducida a todos los idiomas en los que se utilizará el widget.
La estructura debe ser la misma en todos los archivos.
{
"widget": {
"name": "Cumpleaños",
"short_description": "Widget Feliz Cumpleaños",
"description": "El widget de Feliz Cumpleaños asigna una tarea el día de cumpleaños del cliente, así nunca te olvides de una fecha importante.",
"tour_description": "El widget de Feliz Cumpleaños asigna una tarea el día de cumpleaños del cliente, así nunca te olvides de una fecha importante."
},
"settings": {
"custom": "Ajustes",
"options": {
"title": "Ajustes",
"desc": "Configura el formulario para crear una nueva tarea"
},
"base": "Crea automáticamente tareas dentro de un perfil seleccionado el día del cumpleaños de tu cliente.",
"not_active": "Instala el widget para empezar la instalación",
"lead": "Lead",
"contact": "Contacto",
"field": {
"title": "Campo de cumpleaños",
"desc": "Elige un campo que tenga la fecha de cumpleaños",
"tip": "Elige un tipo de campo \"fecha\", \"cumpleaños\" o \"fecha y hora\", o crea uno nuevo con un clic en \"Añadir nuevo campo\"",
"or": "o",
"create": "Añadir nuevo campo"
},
"entity": {
"options": {
"contact": "Contacto",
"lead": "Lead"
}
},
"type": {
"title": "Tipo de tarea",
"desc": "Elige el tipo de tarea a asignar "
},
"responsible": {
"title": "Usuario responsable",
"desc": "Asigna un usuario responsable para esta tarea",
"select": "Usuarios seleccionados ",
"numeral": "usuario,usuario,usuarios",
"current": "Actual usuario responsable"
},
"text": {
"title": "Descripción de la tarea",
"desc": "Escribe una descripción o deja el campo vacio",
"placeholder": "Descripción de la tarea..."
},
"template": {
"title": "Plantillas de chat",
"desc": "Crea una plantilla de chat para enviar a tus clientes",
"name": {
"title": "Nombre de la plantilla",
"desc": "Escribe el nombre de la plantilla que se mostrará en el perfil"
},
"text": {
"title": "Mensaje de texto ",
"desc": "Escribe un mensaje de texto",
"placeholder": "Mensaje... "
},
"list": {
"title": "Lista de plantillas creadas"
},
"create": "Crear una plantilla"
},
"errors": {
"template_fields_required": "Ingresa el nombre y el mensaje de texto para continuar."
},
"modal": {
"title": "Crear nuevo campo Cumpleaños",
"name": {
"title": "Nombre del campo",
"desc": "Ingresa el nombre del campo"
},
"entity": {
"title": "Tipo de objeto",
"desc": "Selecciona el tipo de objeto"
},
"success_saved": "El nuevo campo Cumpleaños se ha creado correctamente.",
"create": "Crear un campo"
},
"widget_panel": {
"birthday": "¡Hoy tu cliente está cumpliendo años!",
"not_birthday": "No hay cumpleaños hoy. ¡Vuelve mañana!",
"congratulate": " ¡Envíale un saludo!"
}
}
}
classes
Para que el archivo script.js no sea demasiado largo, dividimos algunas partes en classes. De esta manera, es más fácil de leer, editar y gestionar.
Solo no olvides importar importar esas classes al principio del archivo script.js.
define(
[
'./classes/template.js',
'./classes/loader.js',
'./classes/kommo.js',
'./classes/events.js',
'./classes/settings.js',
'./plugins/jquery.serializejson.min.js',
]
./classes/cache.js
¿Qué sucede en el archivo cache.js?
return class Cache {
constructor () {
window.KommoWidget.cache = window.KommoWidget.cache || {}
}
Cuando se crea un objeto Cache, se verifica si ya existe un objeto cache en la propiedad KommoWidget
del objeto window. Si el objeto cache no existe, se crea un objeto cache vacío.
Las siguientes claves:valores se almacenan en la caché.
kbd_account :
{
payload: {
id: number; //ID de cuenta
name: string; //Nombre de cuenta
subdomain: string;
created_at: number; //unix timeStamp
created_by: number;
updated_at: number; //unix timeStamp
updated_by: number;
current_user_id: number;
country: string;
currency: string;
currency_symbol: string;
customers_mode: string;
is_unsorted_on: boolean;
mobile_feature_version: number;
is_loss_reason_enabled: boolean;
is_helpbot_enabled: boolean;
is_technical_account: boolean;
contact_name_display_order: number;
_links: { self: { href: string } }; // Solicitud GET a la cuenta
_embedded: {
task_types: [
{
id: number;
name: string;
color: null;
icon_id: null;
code: string;
}
];
};
};
expires: number; //unix timeStamp
}
A continuación, echemos un vistazo a los métodos de Cache.
getItem(key)
Es el método que se utiliza para recuperar un valor de la caché basado en una clave dada.
getItem(key) {
let result;
if (window.KommoWidget.cache[key]) {
result = window.KommoWidget.cache[key];
} else {
let cache = JSON.parse(window.sessionStorage.getItem(key) || null);
if (cache !== null && cache.expires <= Math.floor(Date.now() / 1000)) {
window.sessionStorage.removeItem(key);
}
result = (cache || {}).payload || null;
}
return result;
}
Verifica si el valor para key
existe en el objeto de caché en el KommoWidget
. Si existe, se retorna.
Si el valor no está en la caché, el método verifica sessionStorage
. Si el valor se encuentra en sessionStorage
y no ha expirado, se recupera y se retorna. Si el valor ha expirado, se elimina de sessionStorage
. De lo contrario, se retorna null.
setItem(key, value, expires, local)
El método se utiliza para asignar un valor a la caché.
setItem(key, value, expires, local) {
if (local) {
window.KommoWidget.cache[key] = value;
} else {
window.sessionStorage.setItem(
key,
JSON.stringify({
payload: value,
expires: Math.floor(Date.now() / 1000) + expires,
})
);
}
}
Si local
es true, el valor se almacena directamente en el objeto de caché en el KommoWidget
. De lo contrario, el valor se almacena en sessionStorage
. En ambos casos, el valor se serializa a JSON y se almacena junto con el valor del período de expiración.
removeItem(key)
El método se utiliza para eliminar un valor de sessionStorage
basado en una clave dada.
removeItem (key) {
window.sessionStorage.removeItem(key)
}
./classes/http.js
Lo primero que hacemos es importar cache.js.
define(
[
'./cache.js'
],
request (type, payload, method, options = {})
request(type, payload, method, options = {}) {
let cache = new Cache();
return new Promise(function (resolve, reject) {
let data = null;
if (options.cache) {
data = cache.getItem(options.cache.key);
}
if (!data) {
$.ajax({
url: options.baseURL + type,
data: payload,
method: method,
beforeSend: function (xhr) {
xhr.withCredentials = true;
},
headers: options.headers || {},
})
.done(function (data) {
resolve(data);
})
.fail(function (resp) {
reject(resp);
});
} else {
resolve(data);
}
}).then(function (data) {
return new Promise(function (resolve) {
if (options.embedded && (data || {})["_embedded"]) {
data = (data || {})["_embedded"][options.embedded] || [];
}
if (options.cache && data) {
cache.setItem(
options.cache.key,
data,
options.cache.expires,
options.cache.local || false
);
}
if (options.rk) {
data = (data || {})[options.rk] || null;
}
resolve(data);
});
});
}
El método realiza una solicitud HTTP. Acepta un tipo de solicitud (type
), los datos a enviar (payload
), el método de solicitud (method
) y los parámetros (options
). Si la opción de caché está especificada en los parámetros, se intenta recuperar los datos de la caché. Si no hay datos en la caché, se ejecuta una solicitud AJAX.
./classes/kommo.js
Lo primero que hacemos es importar http.js.
define(
[
'./http.js',
],
getAccount ()
getAccount() {
return this.http.request(
"/api/v4/account",
{ with: "task_types" },
"GET",
{
cache: { key: "kbd_account", expires: 60 },
baseURL: window.location.origin,
}
);
}
El método utiliza el método request() del objeto http para realizar una solicitud HTTP y obtener la información de la cuenta.
getTasks(filter)
getTasks(filter) {
return this.http
.request(
"/api/v4/tasks",
{
filter: filter,
},
"GET",
{
baseURL: window.location.origin,
}
)
.then(function (data) {
return ((data || {})._embedded || {}).tasks || [];
});
}
El método utiliza el método http
del objeto request()
para realizar una solicitud HTTP y recuperar las tareas con el filtro dado.
createTask(payload)
createTask(payload) {
return this.http
.request("/api/v4/tasks", JSON.stringify([payload]), "POST", {
baseURL: window.location.origin,
})
.then(function (data) {
return ((data || {})._embedded || {}).tasks || [];
});
El método utiliza el método http
del objeto request()
para realizar una solicitud HTTP y crear una nueva tarea. Envía una solicitud POST al punto de enlace '/api/v4/tasks'
, pasando los datos de la nueva tarea en el cuerpo..
createField(et, payload)
createField(et, payload) {
let _this = this;
return _this.http
.request(
"/api/v4/" + et + "/custom_fields",
JSON.stringify([payload]),
"POST",
{
baseURL: window.location.origin,
headers: {
"Content-Type": "application/json",
},
}
)
.then(function (data) {
return (((data || {})._embedded || {}).custom_fields || [])[0] || {};
});
}
El método utiliza el método http
del objeto request()
para crear un nuevo campo personalizado para la entidad dada (et
). Envía una solicitud POST al punto de enlace '/api/v4/{et}/custom_fields'
pasando los datos del nuevo campo personalizado en el cuerpo de la solicitud.
createTemplate(payload)
createTemplate(payload) {
return this.http
.request(
"/ajax/v1/chats/templates/add",
JSON.stringify({
request: payload,
}),
"POST",
{
baseURL: window.location.origin,
headers: {
"Content-Type": "application/json",
},
}
)
.then(function (data) {
return (
((((data || {}).response || {}).chats || {}).templates || {})
.added || 0
);
});
}
El método utiliza el método http
del objeto request()
para crear una nueva plantilla de chat. Envía una solicitud POST al punto de enlace '/ajax/v1/chats/templates/add'
pasando los datos de la nueva plantilla en el cuerpo de la solicitud."
getTemplates()
getTemplates() {
return this.http
.request(
"/ajax/v4/chats/templates",
{
with: "integration,reviews",
limit: 50,
page: 1,
},
"GET",
{
baseURL: window.location.origin,
}
)
.then(function (data) {
return ((data || {})._embedded || {}).chat_templates || {};
});
}
El método utiliza el método http
del objeto request()
para obtener una lista de plantillas de chat. Envía una solicitud GET al punto de enlace '/ajax/v4/chats/templates'
.
getUsers(users = [], page = 1)
getUsers(users = [], page = 1) {
let _this = this;
return this.http
.request(
"/api/v4/users",
{
limit: 100,
page: page,
},
"GET",
{
baseURL: window.location.origin,
}
)
.then(function (data) {
return new Promise((resolve) => {
let tmp = ((data || {})._embedded || {}).users || [];
tmp.forEach(function (user) {
if (user.rights.is_active) {
users.push({
id: user.id,
option: user.name,
name: user.name,
is_admin: user.rights.is_admin,
});
}
});
if (data._page_count > 1 && data._page < data._page_count) {
resolve(_this.getUsers(users, page + 1));
} else {
resolve(users);
}
});
El método utiliza el método http
del objeto request()
para obtener una lista de usuarios. Envía una solicitud GET al punto de enlace '/api/v4/users'
agregando todos los usuarios a un arreglo de usuarios.
getTaskTypes()
getTaskTypes() {
let _this = this;
return this.getAccount().then(function (account) {
let types = ((account || {})._embedded || {}).task_types || {};
types = Object.values(types).filter(function (item) {
item.option = item.name;
return item;
});
return types;
});
}
El método se utiliza para obtener una lista de tipos de tareas a partir de la información de la cuenta. Primero, llama al método getAccount()
para obtener la información de la cuenta y luego extrae los tipos de tareas de los datos recuperados.
getFields(et, page = 1, fields = [])
getFields(et, page = 1, fields = []) {
let _this = this;
return _this.http
.request(
"/api/v4/" + et + "/custom_fields",
{
page: page,
},
"GET",
{
baseURL: window.location.origin,
}
)
.then(function (data) {
let cf = ((data || {})._embedded || {}).custom_fields || [];
if (cf.length === 0) {
return fields;
}
fields = fields.concat(cf);
if (((data || {})._page_count || 0) > 1) {
page++;
return _this.getFields(et, page, fields);
} else {
return fields;
}
});
}
El método utiliza el método http
del objeto request()
para obtener una lista de campos personalizados para la entidad dada (et
). Envía una solicitud GET al punto de enlace '/api/v4/{et}/custom_fields'
,recopilando todos los campos en un arreglo de campos.
getEmptyPromise()
getEmptyPromise() {
return new Promise(function (resolve) {
resolve([]);
});
}
El método se utiliza en la siguiente función, la cual debe devolver una Promesa. Pero si no hay nada que devolver, llama a getEmptyPromise()
.
getFieldsByType(fieldTypes, entityType = null, addPostfix = false)
getFieldsByType(fieldTypes, entityType = null, addPostfix = false) {
let _this = this;
let entityTypes = [];
if (entityType) {
entityTypes = !Array.isArray(entityType) ? [entityType] : entityType;
}
return Promise.all([
$.inArray(APP.element_types.leads, entityTypes) >= 0
? _this.getFields("leads")
: _this.getEmptyPromise(),
$.inArray(APP.element_types.contacts, entityTypes) >= 0
? _this.getFields("contacts")
: _this.getEmptyPromise(),
]).then(function ([leadFields, contactFields]) {
let fields = [];
if (!Array.isArray(fieldTypes)) {
fieldTypes = [fieldTypes];
}
entityTypes.forEach(function (et) {
let cf = {};
let postfix = "";
switch (et) {
case APP.element_types.contacts:
cf = contactFields;
postfix += " (" + _this.widget.i18n("settings.contact") + ")";
break;
case APP.element_types.leads:
cf = leadFields;
postfix += " (" + _this.widget.i18n("settings.lead") + ")";
break;
}
if (!addPostfix) {
postfix = "";
}
if (cf.length > 0) {
cf.forEach(function (field) {
if (
APP.cf_types[field.type] &&
$.inArray(APP.cf_types[field.type], fieldTypes) >= 0
) {
fields.push({
id: field.id,
code: (field.code || "").toLowerCase(),
sort: field.sort,
option: field.name + postfix,
type: APP.cf_types[field.type],
entity_type: et,
parent_id: 0,
enums: field.enums || [],
is_hidden: false,
});
}
});
}
});
return fields;
});
}
El método se utiliza para obtener campos personalizados de tipos específicos. Envía consultas para recuperar campos de varios tipos de entidad (contactos, leads) y devuelve un arreglo de objetos de campos que coinciden con los criterios dados.
./classes/events.js
Primero, importamos la librería moment y el módulo 'lib/components/base/modal'
. Estas dependencias son nuestros módulos del sistemaque puedes utilizar.
define(
[
'moment',
'lib/components/base/modal',
],
settings()
settings() {
let _this = this;
const MODAL_DESTROY_TIMEOUT = 3000;
$(".kommo-birthday__field-create__link").on("click", function () {
let modal = _this.widget.templates.twig.modal(
_this.widget.templates.render("settings.modal", {
prefix: _this.widget.config.prefix,
langs: _this.widget.i18n("settings.modal"),
name: _this.widget.templates.twig.input({
block: "field",
code: "name",
}),
entity: _this.widget.templates.twig.select({
block: "field",
code: "entity",
items: [
{
id: APP.element_types.contacts,
option: _this.widget.i18n("settings.entity.options.contact"),
},
{
id: APP.element_types.leads,
option: _this.widget.i18n("settings.entity.options.lead"),
},
],
selected: APP.element_types.contacts,
}),
button: _this.widget.templates.twig.button({
block: "field",
code: "btn",
text: _this.widget.i18n("settings.modal.create"),
}),
}),
function () {},
_this.widget.config.prefix + "__field-modal"
);
$("#" + _this.widget.config.prefix + "-field-btn-id").on(
"click",
function () {
const resultModal = new Modal();
let selected = {};
let form =
$(
"#" + _this.widget.config.prefix + "__field-form"
).serializeJSON().params || {};
let et = parseInt(form.field.entity) === 1 ? "contacts" : "leads";
_this.widget.kommo
.createField(et, {
type: "birthday",
name: form.field.name,
})
.then(function (result) {
selected = result;
return _this.widget.kommo.getFieldsByType(
[
APP.cf_types.date,
APP.cf_types.date_time,
APP.cf_types.birthday,
],
[APP.element_types.contacts, APP.element_types.leads],
true
);
})
.then(function (fields) {
$("." + _this.widget.config.prefix + "__field-id").replaceWith(
_this.widget.templates.twig.select({
block: "field",
code: "id",
items: fields,
selected: selected.id || 0,
})
);
modal.destroy();
resultModal.showSuccess(
_this.widget.i18n("settings.modal.success_saved"),
false,
MODAL_DESTROY_TIMEOUT
);
})
.catch(() => {
resultModal.showError("", false);
});
}
);
});
if (_this.widget.info.params.templates.length > 0) {
let created = _this.widget.info.params.templates.split(",");
$("#kommo-birthday-templates-list").val(
_this.widget.info.params.templates || ""
);
_this.widget.kommo.getTemplates().then(function (templates) {
created = created.map(function (item) {
return parseInt(item);
});
templates.filter(function (item) {
if ($.inArray(item.id, created) > -1) {
$("#kommo-birthday-templates-list-ul").append(
"<li>" + item.name + "</li>"
);
}
});
});
}
$("#kommo-birthday-template-create-id").on("click", function () {
let name = $("#kommo-birthday-template-name-id").val();
let text = $("#kommo-birthday-template-text-id").val();
if (name.length === 0 || text.length === 0) {
new Modal().showError(
_this.widget.i18n("settings.errors.template_fields_required"),
false
);
} else {
_this.widget.kommo
.createTemplate({
name: name,
reply_name: name,
content: text,
reply_text: text,
is_editable: true,
type: "amocrm",
attachments: [],
buttons: [],
widget_code: null,
client_uuid: null,
creator_logo_url: null,
waba_footer: null,
waba_category: null,
waba_language: null,
waba_examples: {},
reviews: null,
waba_header: null,
waba_selected_waba_ids: [],
})
.then(function (id) {
if (parseInt(id) > 0) {
$("#kommo-birthday-template-text-id").val("");
$("#kommo-birthday-template-name-id").val("");
let old = $("#kommo-birthday-templates-list").val().split(",");
old.push(id);
old = old.filter(function (item) {
return parseInt(item) > 0;
});
$("#kommo-birthday-templates-list").val(old.join(","));
$("#kommo-birthday-templates-list-ul").append(
"<li>" + name + "</li>"
);
}
});
}
});
}
El método procesa eventos, como los clics en el enlace para crear un campo personalizado para el widget (mostrando un modal, renderizando entradas y botones en él, creando un campo personalizado al hacer clic en un botón) y el botón para crear la plantilla.
getBirthdayInfo()
getBirthdayInfo() {
const fieldId = parseInt(
this.widget.getNested(this.widget.info.params, "field.id", "")
);
const $wrap = $('.linked-form__field[data-id="' + fieldId + '"]');
const filtered = $wrap.filter(function () {
const formattedDate = Moment().format(
APP.system.format.date.date_short
);
const dayMonth = $(this).find("input").val().slice(0, -5);
return dayMonth === formattedDate;
});
const currentDate = Moment().format(APP.system.format.date.date);
return {
isBirthday: filtered.length > 0,
currentDate: currentDate,
};
}
Este método recupera la información de cumpleaños de la página actual. Verifica si algún contacto o lead tiene cumple años hoy y retorna la fecha actual.
card()
card() {
const _this = this;
const { isBirthday } = _this.getBirthdayInfo();
if (isBirthday) {
let entityType = parseInt(
_this.widget.getNested(_this.widget.info.params, "entity.type", 2)
);
let responsibles = _this.widget.getNested(
_this.widget.info.params,
"tasks.responsible",
{}
);
if (responsibles[1]) {
responsibles[APP.data.current_card.main_user] =
APP.data.current_card.main_user;
}
if (
entityType === parseInt(APP.data.current_card.element_type) &&
responsibles[APP.constant("user").id]
) {
_this.createTask(isBirthday);
}
}
}
El método verifica si la fecha de hoy coincide con el cumpleaños. Si es así y se cumplen condiciones específicas (el usuario está en el perfil del lead, el usuario es responsable de enviar un mensaje al cliente), llama a un método para crear una nueva tarea.
createTask(isBirthday)
createTask(isBirthday) {
const _this = this;
let { currentDate } = _this.getBirthdayInfo();
if (isBirthday) {
const taskType = parseInt(
_this.widget.getNested(_this.widget.info.params, "tasks.type", 1)
);
_this.widget.kommo
.getTasks({
is_completed: 0,
entity_type: APP.data.current_entity,
entity_id: APP.data.current_card.id,
task_type: taskType,
})
.then(function (tasks) {
if (tasks.length === 0) {
_this.widget.kommo.createTask({
responsible_user_id: APP.constant("user").id,
entity_id: APP.data.current_card.id,
entity_type: "leads",
task_type_id: taskType,
text: _this.widget.getNested(
_this.widget.info.params,
"tasks.text",
"-"
),
complete_till: Moment(
(currentDate += " 23:59"),
APP.system.format.date.full
).unix(),
});
}
});
}
}
Si el cumpleaños es hoy y no se han creado tareas, este método creará una nueva tarea utilizando la información del cumpleaños y los parámetros de la tarea.
./classes/loader.js
prepend(elem)
prepend (elem) {
elem.prepend(this.getHtml())
return this
}
El método añade el contenido del indicador de carga HTML antes del elemento elem
especificado.
append(elem)
append (elem) {
elem.append(this.getHtml())
return this
}
El método añade el contenido del indicador de carga HTML después del elemento elem
especificado.
getHtml()
getHtml() {
let _this = this
if (_this.html.length === 0) {
_this.html = _this.templates.render(
'loader',
{
widget: _this.langs.widget.name,
icons: _this.widget.config.icons,
},
)
}
return _this.html
}
El método obtiene el contenido HTML del indicador de carga. Si el HTML aún no ha sido generado, el método renderizará la plantilla twig del indicador de carga.
hide()
hide () {
return $('.kommo-loader').hide()
}
El método accede a los elementos con la clase '.kommo-loader'
y los oculta.
show()
show () {
$('.kommo-loader').show()
return this
}
El método accede a los elementos con la clase "kommo-loader" y los muestra.
remove()
remove () {
$('.kommo-loader').remove()
return this
}
El método accede a los elementos con la clase "kommo-loader" y los elimina.
displaySaveBtn(code)
displaySaveBtn(code) {
$(".modal." + code)
.find(".widget_settings_block__controls")
.show();
return this;
}
El método muestra un botón de guardar en una ventana modal con la clase de código especificada.
./classes/settings.js
save(evt)
save (evt) {
let _this = this
let code = _this.widget.params.widget_code
let isActive = false
let params = ($('#' + _this.widget.config.prefix + '-settings__form').serializeJSON() || {}).params || {}
return new Promise(function (resolve, reject) {
isActive = evt.active === 'Y'
let data = {
is_active: isActive,
}
let installed = ((_this.widget.params || {}).active || 'N') === 'Y'
if (!installed) {
resolve({ 'reinstall': true })
}
if (isActive) {
if (!_this.widget.validateSettings(params)) {
$('.modal.' + code).
find('.js-widget-save').
trigger('button:save:error')
reject()
} else {
data.params = params
}
}
_this.widget.info = data
evt.fields.custom = JSON.stringify(params)
resolve(data)
}).then(function () {
return true
})
}
El método se utiliza para guardar la configuración del widget. Maneja el evento evt
y retorna una Promise
. Dentro del método, verifica el estado del widget (isActive
), recopila los datos del formulario de configuración (params
), crea un objeto (data
) con información sobre el estado del widget, y maneja el caso cuando el widget fue desinstalado. Después de procesar, el método retorna el objeto data
cuando el guardado es exitoso.
load()
load () {
let _this = this
return new Promise((resolve, reject) => {
_this.widget.info.params = (_this.widget.params || {}).custom || {}
if (typeof _this.widget.info.params === 'string') {
_this.widget.info.params = JSON.parse(_this.widget.info.params)
}
if (!_this.widget.info.params.templates) {
_this.widget.info.params.templates = ''
}
resolve([])
})
}
El método se utiliza para cargar la configuración del widget. Devuelve una Promise
y realiza la configuración preliminar de los parámetros del widget, como params.templates
.
./classes/template.js
El archivo classes/template.js
, que se utiliza para renderizar plantillas, es simplemente un contenedor alrededor de los métodos this.render
propios del widget. Esto tiene como objetivo simplificar un poco el código, pero no significa que debas utilizar esta implementación específica. Puedes ceñirte a lo básico o crear tu propia solución. Lo que se proporciona aquí es un ejemplo que se puede mejorar."
Primero, importamos las librerías jquery, twigjs, text y un módulo 'lib/components/base/modal'
.
define(
[
'jquery',
'lib/components/base/modal',
'twigjs',
'text',
]
flushTextPlugin()
flushTextPlugin () {
text.useXhr = function () {
return true
}
}
El método se utiliza para cambiar temporalmente la configuración para cargar archivos de texto, de modo que siempre se carguen a través de XHR.
restoreTextPlugin()
restoreTextPlugin () {
text.useXhr = this.textSavedXhr
}
El método se utiliza para restaurar la configuración predeterminada para cargar archivos de texto después de un cambio temporal (flushTextPlugin()
).
checkRegistry(name)
checkRegistry (name) {
let id = 'kommo_bd_' + name
return !!(Twig.Templates || {}).registry[id]
}
El método se utiliza para verificar la existencia de una plantilla Twig.
getFromRegistry(name)
getFromRegistry (name) {
let id = 'kommo_bd_' + name
return (Twig.Templates || {}).registry[id] || ''
}
El método se utiliza para recuperar una plantilla Twig por su nombre.
preload()
preload () {
return Promise.all([
this.loadCss(),
this.loadTemplates(),
],
)
}
El método se utiliza para precargar plantillas y archivos CSS. Devuelve una Promise
que se resuelve después de que todos los recursos hayan sido cargados.
loadTemplates()
loadTemplates() {
let _this = this;
return new Promise(function (resolve) {
let area = APP.widgets.system.area;
let templates = _this.templates.params[area] || [];
_this.flushTextPlugin();
if (templates.length > 0) {
let load = [];
let ids = [];
templates.forEach(function (template) {
if (template.id.indexOf(_this.widget.config.code) === -1) {
template.id = _this.widget.config.code + "_" + template.id;
}
if (!template.url) {
template.url =
_this.widget.params.path +
"/assets/templates" +
template.path +
".twig?v=" +
_this.widget.get_version();
}
if (!_this.checkRegistry(template.id)) {
load.push("text!" + template.url);
ids.push(template.id);
} else {
_this.templates.html[template.id] = _this.getFromRegistry(
template.id
);
}
});
if (load.length > 0) {
require(load, function () {
for (let i = 0; i < arguments.length; i++) {
_this.addTemplate(ids[i], arguments[i]);
}
resolve();
});
} else {
resolve();
}
} else {
resolve();
}
});
}
El método se utiliza para cargar plantillas. Este método utiliza APP.widgets.system.area
para determinar el área actual dentro de la aplicación. Retorna una Promise que se cumple después de que todas las plantillas hayan sido cargadas.
addTemplate(name, data)
addTemplate(name, data) {
let id = "kommo_bd_" + name;
if (this.checkRegistry(name)) {
this.templates.html[name] = Twig.Templates.registry[id];
return;
}
this.templates.html[name] = twig({
id: id,
data: data,
allowInlineIncludes: true,
});
}
El método se utiliza para añadir una plantilla a Twig. Si ya existe una plantilla con el nombre proporcionado en el registro, esta se actualiza; de lo contrario, se crea una nueva.
loadCss()
loadCss() {
let _this = this;
return new Promise(function (resolve) {
let html = "";
_this.css.forEach((file) => {
let $style = null;
let path =
_this.widget.params.path +
"/assets/css/" +
file.name +
".css?v=" +
_this.widget.params.version;
if (file.append_id) {
$style = $("#" + file.append_id);
} else {
$style = $('link[href="' + path + '"]');
}
if ($style.length < 1) {
html +=
'<link type="text/css" rel="stylesheet" href="' + path + '"';
if (file.append_id) {
html += ' id="' + file.append_id + '"';
}
html += ">";
}
});
if (html.length > 0) {
$("head").append(html);
}
resolve();
});
}
El método se utiliza para cargar archivos CSS. Retorna una Promise que se cumple después de que todos los archivos CSS hayan sido cargados.
render(name, params)
render(name, params) {
name = this.widget.config.code + "_" + name;
return this.templates.html[name].render(params || {});
}
El método se utiliza para renderizar una plantilla por su nombre y para pasarle parámetros.
installPlaceholder(wrapDiv, exception = null)
installPlaceholder(wrapDiv, exception = null) {
let params = {
prefix: this.widget.config.prefix,
langs: this.widget.i18n("settings"),
active: false,
};
if (exception !== null) {
params.exception = exception;
}
wrapDiv.prepend(this.render("settings.base", params));
$("#kommo-settings").fadeIn(300);
this.widget.loader.displaySaveBtn(this.widget.params.widget_code);
return this.widget.loader.hide();
}
El método se utiliza para instalar un placeholder en una ubicación específica de la página, según lo indicado por el argumento wrapDiv
. El argumento exception
se puede utilizar para especificar un elemento que debe ser excluido de la instalación del placeholder.
get twig ()
El método devuelve un objeto con métodos para generar varios elementos de la interfaz de usuario basados en plantillas Twig de Kommo.
plugins
Solo hay un archivo en esta carpeta, es jquery.serializejson.min.js
.
Este plugin está diseñado para serializar los datos del formulario en formato JSON. Permite convertir los datos ingresados por el usuario en un formulario a JSON.
assets
Cuando abres la carpeta assets, verás otras dos carpetas: css y templates.
css
Tenemos dos archivos CSS en la carpeta (kommo.css y style.css) por varias razones, como la organización del código (hacer los estilos más legibles), la modularización (estilos separados para diferentes partes del código), la reutilización (por ejemplo, los archivos con estilos comunes pueden ser incluidos en varios proyectos para mantener una apariencia coherente), la gestión de dependencias (utilizar varios archivos CSS facilita la gestión de dependencias entre los estilos, ya que puedes agregar o eliminar archivos sin necesidad de cambiar todo el proyecto), y la optimización de la carga de la página (dividir los estilos en varios archivos permite que el navegador cargue solo los archivos CSS necesarios para una página o componente específico).
Cómo se implementa la visualización de un globo al pasar el cursor sobre una imagen
.kommo__tooltip-wrap:hover .kommo-holdings__settings__tooltip {
visibility: visible !important;
opacity: 1 !important;
}
.kommo__tooltip-wrap:hover .kommo__settings__tooltip, .kommo__tooltip-wrap--icon:hover .kommo__settings__tooltip {
visibility: visible !important;
opacity: 1 !important;
}
Cuando se pasa el cursor sobre un elemento con la clase kommo__tooltip-wrap
, los globos se vuelven visibles y opacos.
plantillas
loader.twig
Esta plantilla representa un bloque de carga que se muestra en la página mientras se carga el contenido o se realizan operaciones asincrónicas.
base.twig
Esta plantilla se utiliza para mostrar la configuración básica del widget.
modal.twig
Esta plantilla se utiliza para mostrar una ventana modal de configuración.
widget_right_panel.twig
Esta plantilla se utiliza para mostrar un widget en el panel derecho. Toma dos parámetros: "text," que variará dependiendo de si hay una persona que cumple cumpleaños ese día, y un botón (que solo se mostrará si hay una persona que cumple años).
script.js
responsible
responsible: _this.templates.twig.dropdown({
block: 'tasks',
code: 'responsible',
title_empty: _this.i18n('settings.responsible.select'),
title_numeral: _this.i18n(
'settings.responsible.numeral'),
name_is_array: true,
items: users.filter(function (user) {
user.name = 'params[tasks][responsible][' + user.id + ']'
user.is_checked = !!(_this.getNested(
_this.info.params,
'tasks.responsible', {})[user.id]
)
return user
}),
Se añade una lista desplegable, donde los usuarios responsables (usuario) se agregan a la información/parámetros del widget cuando se marca una casilla de verificación.
 `APP.widgets.list`](https://files.readme.io/44cce53-image.png)
Obtenemos esta información de una variable de entorno APP.widgets.list
click
.then(() => {
$(".kommo-birthday__button-congratulate").on(
"click",
function () {
setTimeout(function () {
$(".feed-compose__quick-actions-wrapper").click();
}, QUICK_ACTIONS_CLICK_TIMEOUT);
}
);
});
Al hacer clic en el botón ("Enviar un deseo de cumpleaños"), se abrirá una lista de plantillas que se pueden enviar al chat.

El botón funciona correctamente cuando los chats ya están abiertos. Sin embargo, esta funcionalidad no incluye la opción de cambiar a un chat desde otros estados.
Crea una integración privada y sube tu widget
Puedes encontrar el widget en Github.
Para crear una integración privada, debes:
- Ve a Ajustes en el menú izquierdo.
- Haz clic en Integración.
- Haz clic en el botón azul + Crear integración en la esquina superior derecha.

Para subir el archivo del widget, debes hacer clic en el botón Subir.

Antes de subir el archivo con el widget, asegúrate de que todos los archivos estén en la raíz del archivo comprimido.

...¡y eso es todo!
¡Ahora es momento de probar el widget!
Updated 8 days ago