Example Application with Additional Content Blocks

<?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>