Example Application with Additional Content Blocks
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect to the MCP server so that the assistant can utilize the official REST documentation.
<?php
header('Content-Type: text/html; charset=UTF-8');
$placementOptions = json_decode($_REQUEST['PLACEMENT_OPTIONS'] ?? '', true);
$forceMode = ($placementOptions['force_mode'] ?? null) === 'Y';
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@9.9.2/dist/jsoneditor.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/jsoneditor@9.9.2/dist/jsoneditor.min.css" rel="stylesheet">
<script src="//api.bitrix24.com/api/v1/"></script>
</head>
<body>
<div class="container-fluid">
<form id="form" class="mt-3 md-3">
<div class="row">
<div class="col-8">
<div class="mb-3">
<div class="d-flex flex-row gap-3">
<label class="form-label h3">Layout JSON</label>
<div id="content_block_presets" class="d-flex flex-row gap-2"></div>
</div>
<div id="json_editor" style="height: 510px"></div>
<input type="hidden" id="layout" value="{}">
</div>
</div>
<div class="col-4" id="parameters">
<label class="form-label h3">Parameters</label>
<hr class="mt-0">
<div class="vstack gap-3">
<div class="form-group">
<label for="entity_type_id">Parent Entity</label>
<select id="entity_type_id" name="entityTypeId" class="form-select">
<option value="2" selected>[2] Deal</option>
<option value="1">[1] Lead</option>
<option value="3">[3] Contact</option>
<option value="4">[4] Company</option>
<option value="7">[7] Estimate</option>
<option value="31">[31] Invoice </option>
</select>
</div>
<div class="form-group">
<label for="entity_id">Parent Entity ID</label>
<input id="entity_id" name="entityId" type="text" class="form-control">
</div>
<div class="form-group">
<label for="item_type_id" class="text-truncate">Adding configurable blocks to:</label>
<select name="itemTypeId" id="item_type_id" class="form-select" required>
<option value="1" selected>CRM Activity</option>
<option value="2">Timeline Record</option>
</select>
</div>
<?php if (!$forceMode): ?>
<button id="get_items_button" type="button" class="btn btn-outline-dark btn-sm">Find</button>
<?php endif; ?>
<div class="form-group">
<label for="item_id">CRM Activity:</label>
<?php if ($forceMode): ?>
<input id="item_id" name="itemId" type="text" class="form-control">
<?php else: ?>
<select name="itemId" id="item_id" class="form-select"></select>
<?php endif; ?>
</div>
<button id="get_button" type="button" class="btn btn-outline-dark btn-sm">Get</button>
<button id="set_button" type="button" class="btn btn-outline-dark btn-sm">Set</button>
<button id="delete_button" type="button" class="btn btn-outline-danger btn-sm">Delete</button>
</div>
</div>
</div>
</form>
</div>
<div class="container-fluid" id="alert_container"></div>
</body>
</html>
<script>
const ITEM_ACTIVITY = 1;
const ITEM_TIMELINE = 2;
const ALLOWED_ITEM_TYPES = [
ITEM_ACTIVITY,
ITEM_TIMELINE,
];
const METHODS_MAP = {
[ITEM_ACTIVITY]: {
get: 'crm.activity.layout.blocks.get',
set: 'crm.activity.layout.blocks.set',
delete: 'crm.activity.layout.blocks.delete',
itemField: 'activityId',
},
[ITEM_TIMELINE]: {
get: 'crm.timeline.layout.blocks.get',
set: 'crm.timeline.layout.blocks.set',
delete: 'crm.timeline.layout.blocks.delete',
itemField: 'timelineId',
},
};
class ConfigurableTimelineBlocks {
#jsonEditor;
#statusContainer;
#contentBlockPresets;
#isForceMode = false;
// fields
#entityTypeIdNode;
#entityIdNode;
#itemTypeIdNode;
#itemIdNode;
// buttons
#getButton;
#setButton;
#deleteButton;
#getItemsButton
constructor(
jsonEditor,
statusContainer,
contentBlockPresets,
) {
this.#jsonEditor = jsonEditor;
this.#statusContainer = statusContainer;
this.#contentBlockPresets = contentBlockPresets;
this.renderJSONLayoutActions();
this.fetchProperties();
this.bindEvents();
this.loadDynamicTypes();
}
fetchProperties() {
this.#entityTypeIdNode = document.getElementById('entity_type_id');
this.#entityIdNode = document.getElementById('entity_id');
this.#itemTypeIdNode = document.getElementById('item_type_id');
this.#itemIdNode = document.getElementById('item_id');
this.#getButton = document.getElementById('get_button');
this.#setButton = document.getElementById('set_button');
this.#deleteButton = document.getElementById('delete_button');
this.#getItemsButton = !this.#isForceMode ? document.getElementById('get_items_button') : null;
}
bindEvents() {
this.#getButton.onclick = this.getAction.bind(this);
this.#setButton.onclick = this.setAction.bind(this);
this.#deleteButton.onclick = this.deleteAction.bind(this);
if (this.#getItemsButton)
{
this.#getItemsButton.onclick = this.getItemsAction.bind(this);
this.#itemTypeIdNode.onchange = () => {
this.#itemIdNode.innerHTML = '';
const label = document.querySelector(`[for="item_id"]`);
label.textContent = this.getItemTypeId() === ITEM_ACTIVITY ? 'CRM Activity' : 'Timeline Record';
};
}
}
renderJSONLayoutActions() {
const contentBlockPresetsContainer = document.getElementById('content_block_presets');
if (!contentBlockPresetsContainer) {
return;
}
contentBlockPresetsContainer.innerHTML = '';
this.#contentBlockPresets.forEach((contentBlockPreset) => {
const button = document.createElement('button');
button.classList = 'btn btn-link btn-sm text-secondary';
button.innerText = contentBlockPreset.getTitle();
button.type = 'button';
button.onclick = () => {
let json = this.#jsonEditor.get();
if (!json.blocks) {
json.blocks = {};
}
const length = Object.keys(json?.blocks).length;
json.blocks[`${length + 1}`] = contentBlockPreset.getValue();
this.#jsonEditor.set(json);
return false;
};
contentBlockPresetsContainer.append(button);
});
const clearButton = document.createElement('button');
clearButton.innerText = 'Clear';
clearButton.classList = 'btn btn-link btn-sm text-danger';
clearButton.type = 'button';
clearButton.onclick = () => {
this.#jsonEditor.set({});
};
contentBlockPresetsContainer.append(clearButton);
}
loadDynamicTypes()
{
BX24.callMethod('crm.type.list', {}, (result) => {
const types = result?.answer?.result?.types || [];
types.forEach((item) => {
const option = document.createElement("option");
option.value = item.entityTypeId;
option.innerText = `[${item.id}] ${item.title}`;
this.#entityTypeIdNode.append(option);
})
});
}
loading()
{
this.#entityTypeIdNode.disabled = true;
this.#entityIdNode.disabled = true;
this.#itemTypeIdNode.disabled = true;
this.#itemIdNode.disabled = true;
this.#getButton.disabled = true;
this.#setButton.disabled = true;
this.#deleteButton.disabled = true;
}
stopLoading()
{
this.#entityTypeIdNode.disabled = false;
this.#entityIdNode.disabled = false;
this.#itemTypeIdNode.disabled = false;
this.#itemIdNode.disabled = false;
this.#getButton.disabled = false;
this.#setButton.disabled = false;
this.#deleteButton.disabled = false;
}
getItemsAction()
{
if (this.#isForceMode)
{
return;
}
if (!this.validateEntityTypeIdAndEntityId())
{
return;
}
if (this.getItemTypeId() === ITEM_ACTIVITY)
{
const data = {
select: ['*'],
filter: {
'OWNER_TYPE_ID': this.getEntityTypeId(),
'OWNER_ID': this.getEntityId(),
},
};
const callback = (result) => {
if (result.error())
{
this.renderDangerAlert(result.error());
return;
}
const activities = result.data();
this.#itemIdNode.innerHTML = '';
activities.forEach((activity) => {
const option = document.createElement('option');
option.innerText = `[${activity.ID}] ${activity.SUBJECT} | ${activity.PROVIDER_ID}`;
option.value = activity.ID;
this.#itemIdNode.append(option);
});
};
BX24.callMethod('crm.activity.list', data, callback);
return;
}
const data = {
select: ['*'],
filter: {
'bindings': {
'entityTypeId': this.getEntityTypeId(),
'entityId': this.getEntityId(),
},
},
};
const callback = (result) => {
if (result.error())
{
this.renderDangerAlert(result.error());
return;
}
const items = result.data().items;
items.forEach((item) => {
let option = document.createElement('option');
const title = item?.layout?.header?.title ?? 'Undefined';
const id = item.id;
option.value = id;
option.innerText = `[${id}] ${title}`;
this.#itemIdNode.append(option);
});
};
BX24.callMethod('crm.timeline.historyitem.list', data, callback);
}
getAction()
{
const isValid = this.validateFieldsWithAlerts();
if (!isValid)
{
return;
}
const method = this.getMethod('get');
const data = this.getData();
const callback = (result) => {
this.stopLoading();
if (result.error())
{
this.renderDangerAlert(result.error());
return;
}
this.#jsonEditor.set(result.data().layout ?? {});
this.renderSuccessAlert("Done, the result is just above ;)");
};
this.loading();
BX24.callMethod(method, data, callback);
}
setAction()
{
const isValid = this.validateFieldsWithAlerts();
if (!isValid)
{
return;
}
const method = this.getMethod('set');
const data = {
...this.getData(),
layout: this.#jsonEditor.get(),
};
const callback = (result) => {
this.stopLoading();
if (result.error())
{
this.renderDangerAlert(result.error());
return;
}
this.renderSuccessAlert("Additional content blocks have been successfully set ;)");
};
this.loading();
BX24.callMethod(method, data, callback);
}
deleteAction()
{
const isValid = this.validateFieldsWithAlerts();
if (!isValid)
{
return;
}
const method = this.getMethod('delete');
const data = this.getData();
const callback = (result) => {
this.stopLoading();
if (result.error())
{
this.renderDangerAlert(result.error());
return;
}
this.renderSuccessAlert("Additional content blocks have been successfully deleted ;)");
this.#jsonEditor.set({});
};
this.loading();
BX24.callMethod(method, data, callback);
}
validateFieldsWithAlerts()
{
if (!this.validateEntityTypeIdAndEntityId())
{
return false;
}
const itemId = this.getItemId();
if (!itemId)
{
alert('Enter the ID of the CRM Activity/Timeline Record');
this.#itemIdNode.focus();
return false;
}
return true;
}
validateEntityTypeIdAndEntityId()
{
const entityId = this.getEntityId();
if (!entityId)
{
alert('Enter the ID of the Parent Entity');
this.#entityIdNode.focus();
return false;
}
if (!ALLOWED_ITEM_TYPES.includes(this.getItemTypeId()))
{
alert('Select a valid value for the entity type to which the configurable blocks will be added');
this.#itemTypeIdNode.focus();
return false;
}
return true;
}
getData()
{
return {
entityTypeId: this.getEntityTypeId(),
entityId: this.getEntityId(),
[this.getItemFieldName()]: this.getItemId(),
};
}
getMethod(method)
{
return METHODS_MAP[this.getItemTypeId()][method];
}
getEntityTypeId()
{
return Number.parseInt(this.#entityTypeIdNode.value, 10);
}
getEntityId()
{
return Number.parseInt(this.#entityIdNode.value, 10);
}
getItemTypeId()
{
return Number.parseInt(this.#itemTypeIdNode.value, 10);
}
getItemId()
{
return Number.parseInt(this.#itemIdNode.value, 10);
}
getItemFieldName()
{
return METHODS_MAP[this.getItemTypeId()].itemField;
}
renderAlert(message, classList)
{
const alert = document.createElement('div');
alert.className = classList;
alert.setAttribute('role', 'alert');
const time = (new Date()).toLocaleTimeString();
alert.innerText = `[${time}] ${message}`;
this.#statusContainer.innerHTML = '';
this.#statusContainer.append(alert);
}
renderDangerAlert(message)
{
this.renderAlert(message, 'alert alert-danger');
}
renderSuccessAlert(message)
{
this.renderAlert(message, 'alert alert-success')
}
}
class ContentBlockPreset
{
#title;
#value;
constructor(title, value) {
this.#title = title;
this.#value = value;
}
getTitle(){ return this.#title; }
getValue(){ return this.#value; }
}
const presets = [
new ContentBlockPreset('Text', {
type: "text",
properties: {
value: "Hello!\nWe are starting.",
multiline: true,
bold: true,
color: "base_90"
}
}),
new ContentBlockPreset('Large text', {
type: "largeText",
properties: {
value: "Hello!\nWe are starting.\nWe are continuing.\nWe are still working on this.\nWe are continuing.\nWe are close to the result.\nGoodbye."
}
}),
new ContentBlockPreset('Link', {
type: "link",
properties: {
text: "Open Deal",
action: {
type: "redirect",
uri: "/crm/deal/details/123/"
},
bold: true
}
}),
new ContentBlockPreset('With title', {
type: "withTitle",
properties: {
title: "Title",
block: {
type: "text",
properties: {
value: "Some value"
}
}
}
}),
new ContentBlockPreset('With title (inline)', {
type: "withTitle",
properties: {
title: "Title 2",
block: {
type: "link",
properties: {
text: "Open Deal",
action: {
type: "redirect",
uri: "/crm/deal/details/123/"
}
}
},
inline: true
}
}),
new ContentBlockPreset('Line of blocks', {
type: "lineOfBlocks",
properties: {
blocks: {
text: {
type: "text",
properties: {
value: "Some text"
}
},
link: {
type: "link",
properties: {
text: "link",
action: {
type: "redirect",
uri: "/crm/deal/details/123/"
}
}
},
boldText: {
type: "text",
properties: {
value: "bold text",
bold: true
}
}
}
}
}),
new ContentBlockPreset('Deadline', {
type: "deadline",
properties: {
readonly: false
}
}),
];
document.addEventListener("DOMContentLoaded", () => {
const alertContainer = document.getElementById('alert_container');
const jsonEditor = new JSONEditor(document.getElementById('json_editor'), {
mode: 'code',
});
new ConfigurableTimelineBlocks(
jsonEditor,
alertContainer,
presets,
);
});
</script>