Пример приложения с дополнительными контентными блоками
<?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">Параметры</label> <hr class="mt-0"> <div class="vstack gap-3"> <div class="form-group"> <label for="entity_type_id">Родительская сущность</label> <select id="entity_type_id" name="entityTypeId" class="form-select"> <option value="2" selected>[2] Сделка</option> <option value="1">[1] Лид</option> <option value="3">[3] Контакт</option> <option value="4">[4] Компания</option> <option value="7">[7] Предложение</option> <option value="31">[31] Счёт </option> </select> </div> <div class="form-group"> <label for="entity_id">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">Добавляем конфигурируемые блоки в:</label> <select name="itemTypeId" id="item_type_id" class="form-select" required> <option value="1" selected>Дело</option> <option value="2">Запись таймлайна</option> </select> </div> <?php if (!$forceMode): ?> <button id="get_items_button" type="button" class="btn btn-outline-dark btn-sm">Найти</button> <?php endif; ?> <div class="form-group"> <label for="item_id">Дело:</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">Получить</button> <button id="set_button" type="button" class="btn btn-outline-dark btn-sm">Установить</button> <button id="delete_button" type="button" class="btn btn-outline-danger btn-sm">Удалить</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 ? 'Дело' : 'Запись таймлайна'; }; } } 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("Готово, результат чуть выше ;)"); }; 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("Дополнительные контентные блоки успешно установлены ;)"); }; 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("Дополнительные контентные блоки успешно удалены ;)"); this.#jsonEditor.set({}); }; this.loading(); BX24.callMethod(method, data, callback); } validateFieldsWithAlerts() { if (!this.validateEntityTypeIdAndEntityId()) { return false; } const itemId = this.getItemId(); if (!itemId) { alert('Введите ID Дела/Записи таймлайна'); this.#itemIdNode.focus(); return false; } return true; } validateEntityTypeIdAndEntityId() { const entityId = this.getEntityId(); if (!entityId) { alert('Введите ID Родительской сущности'); this.#entityIdNode.focus(); return false; } if (!ALLOWED_ITEM_TYPES.includes(this.getItemTypeId())) { alert('Выберите корректное значение для типа сущности к которой будут добавлены конфигурационные блоки'); 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: "Здравствуйте!\nМы начинаем.", multiline: true, bold: true, color: "base_90" } }), new ContentBlockPreset('Large text', { type: "largeText", properties: { value: "Здравствуйте!\nМы начинаем.\nМы продолжаем.\nМы все еще работаем над этим.\nМы продолжаем.\nМы близки к результату.\nДо свидания." } }), new ContentBlockPreset('Link', { type: "link", properties: { text: "Открыть сделку", action: { type: "redirect", uri: "/crm/deal/details/123/" }, bold: true } }), new ContentBlockPreset('With title', { type: "withTitle", properties: { title: "Заголовок", block: { type: "text", properties: { value: "Какое-то значение" } } } }), new ContentBlockPreset('With title (inline)', { type: "withTitle", properties: { title: "Заголовок 2", block: { type: "link", properties: { text: "Открыть сделку", action: { type: "redirect", uri: "/crm/deal/details/123/" } } }, inline: true } }), new ContentBlockPreset('Line of blocks', { type: "lineOfBlocks", properties: { blocks: { text: { type: "text", properties: { value: "Какой-то текст" } }, link: { type: "link", properties: { text: "ссылка", action: { type: "redirect", uri: "/crm/deal/details/123/" } } }, boldText: { type: "text", properties: { value: "жирный текст", 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>
© «Битрикс», 2001-2024, «1С-Битрикс», 2024