Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
.dwc--generator-content-input {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dwc--generator-toolbar {
|
||||
background-color: var(--layer);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.dwc--generator-content-editable {
|
||||
background-color: var(--layer);
|
||||
padding: 3px 14px;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.dwc--generator-wrapper {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.dwc--generator-copy {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.dwc--generator-content-code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
Mautic.toggleDwcFilters = function () {
|
||||
mQuery("#dwcFiltersTab, #slotNameDiv").toggleClass("hide");
|
||||
if (mQuery("#dwcFiltersTab").hasClass('hide')) {
|
||||
mQuery('.nav-tabs a[href="#details"]').click();
|
||||
} else {
|
||||
Mautic.dynamicContentOnLoad();
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.dwcGenerator = (function() {
|
||||
// Selectors
|
||||
const copyBtnSelector = '#generator-copy-dynamic-content-slot';
|
||||
const pluginTabSelector = '#dwc--generator-plugins';
|
||||
const htmlTabSelector = '#dwc--generator-html';
|
||||
const codeContainerSelector = '.dwc--generator-content-code';
|
||||
const inputSelector = '.dwc--generator-content-input';
|
||||
const switchCodeWrapperBtnSelector = '#generator-switch-code-wrapper';
|
||||
const switchHtmlTagBtnSelector = '#generator-switch-html-tag';
|
||||
|
||||
// State variables
|
||||
let isPluginBracketMode = true; // True means {mautic ...}, false means [mautic ...]
|
||||
let isUsingDiv = true; // True means using <div> for HTML snippet, false means using <span>
|
||||
|
||||
// Determine the active tab (plugin or HTML)
|
||||
const getActiveTabType = () => {
|
||||
const activePane = document.querySelector('.tab-pane.active.in') || document.querySelector('.tab-pane.active');
|
||||
if (!activePane) return 'plugin'; // Default to 'plugin' if none found
|
||||
return activePane.id === 'dwc--generator-plugins' ? 'plugin' : 'html';
|
||||
};
|
||||
|
||||
const toggleState = (currentState) => !currentState;
|
||||
|
||||
// Restore input value after content changes
|
||||
const updateInputValue = (container, inputValue) => {
|
||||
const input = container.querySelector(inputSelector);
|
||||
if (input && inputValue !== null) input.value = inputValue;
|
||||
};
|
||||
|
||||
// Toggle between {mautic ...} and [mautic ...] in the plugin tab
|
||||
const switchCodeWrapper = () => {
|
||||
const pluginTab = document.querySelector(pluginTabSelector);
|
||||
if (!pluginTab) return;
|
||||
|
||||
const pre = pluginTab.querySelector(codeContainerSelector);
|
||||
if (!pre) return;
|
||||
|
||||
const input = pluginTab.querySelector(inputSelector);
|
||||
const inputValue = input ? input.value : '';
|
||||
let code = pre.innerHTML;
|
||||
|
||||
if (isPluginBracketMode) {
|
||||
// Convert {mautic ...} to [mautic ...]
|
||||
code = code
|
||||
.replace(/\{mautic/g, '[mautic')
|
||||
.replace(/slot=".*?"\}/g, match => match.replace('}', ']')) // Replace } with ] in the first part
|
||||
.replace(/\{\/mautic\}/g, '[/mautic]');
|
||||
} else {
|
||||
// Convert [mautic ...] to {mautic ...}
|
||||
code = code
|
||||
.replace(/\[mautic/g, '{mautic')
|
||||
.replace(/slot=".*?"\]/g, match => match.replace(']', '}')) // Replace ] with } in the first part
|
||||
.replace(/\[\/mautic\]/g, '{/mautic}');
|
||||
}
|
||||
|
||||
pre.innerHTML = code;
|
||||
updateInputValue(pluginTab, inputValue);
|
||||
isPluginBracketMode = toggleState(isPluginBracketMode);
|
||||
};
|
||||
|
||||
// Toggle between <div> and <span> in the HTML tab
|
||||
const switchHtmlTag = () => {
|
||||
const htmlTab = document.querySelector(htmlTabSelector);
|
||||
if (!htmlTab) return;
|
||||
|
||||
const pre = htmlTab.querySelector(codeContainerSelector);
|
||||
if (!pre) return;
|
||||
|
||||
const input = htmlTab.querySelector(inputSelector);
|
||||
const inputValue = input ? input.value : '';
|
||||
let code = pre.innerHTML;
|
||||
|
||||
// Replace HTML tags based on the current state
|
||||
code = isUsingDiv
|
||||
? code.replace(/<div/g, '<span').replace(/<\/div>/g, '</span>')
|
||||
: code.replace(/<span/g, '<div').replace(/<\/span>/g, '</div>');
|
||||
|
||||
pre.innerHTML = code;
|
||||
updateInputValue(htmlTab, inputValue);
|
||||
isUsingDiv = toggleState(isUsingDiv);
|
||||
};
|
||||
|
||||
// Copy the current code snippet to the clipboard, customized for the active tab
|
||||
const copyCode = () => {
|
||||
const activeTab = getActiveTabType();
|
||||
const container = document.querySelector(
|
||||
`${activeTab === 'plugin' ? pluginTabSelector : htmlTabSelector} ${codeContainerSelector}`
|
||||
);
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const input = container.querySelector(inputSelector);
|
||||
const userValue = (input ? input.value : '').trim();
|
||||
let code;
|
||||
|
||||
if (activeTab === 'plugin') {
|
||||
const slotMatch = container.textContent.match(/slot="([^"]+)"/);
|
||||
const slotName = slotMatch ? slotMatch[1] : '';
|
||||
const wrapper = isPluginBracketMode
|
||||
? { open: `{mautic type="content" slot="${slotName}"}`, close: '{/mautic}' }
|
||||
: { open: `[mautic type="content" slot="${slotName}"]`, close: '[/mautic]' };
|
||||
|
||||
code = `${wrapper.open}${userValue}${wrapper.close}`.trim();
|
||||
} else {
|
||||
const tag = isUsingDiv ? 'div' : 'span';
|
||||
const slotMatch = container.innerHTML.match(/data-param-slot-name="([^"]+)"/);
|
||||
const slotName = slotMatch ? slotMatch[1] : '';
|
||||
code = `<${tag} data-slot="dwc" data-param-slot-name="${slotName}">${userValue}</${tag}>`;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const flashMessage = Mautic.addInfoFlashMessage(Mautic.translate('mautic.core.copied'));
|
||||
Mautic.setFlashes(flashMessage);
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize event listeners for all buttons
|
||||
const init = () => {
|
||||
const copyBtn = document.querySelector(copyBtnSelector);
|
||||
if (copyBtn) copyBtn.addEventListener('click', copyCode);
|
||||
|
||||
const switchCodeBtn = document.querySelector(switchCodeWrapperBtnSelector);
|
||||
if (switchCodeBtn) switchCodeBtn.addEventListener('click', switchCodeWrapper);
|
||||
|
||||
const switchTagBtn = document.querySelector(switchHtmlTagBtnSelector);
|
||||
if (switchTagBtn) switchTagBtn.addEventListener('click', switchHtmlTag);
|
||||
};
|
||||
|
||||
return { init };
|
||||
})();
|
||||
|
||||
Mautic.toggleContentEditor = function () {
|
||||
const typeField = mQuery('#dwc_type').val();
|
||||
const contentField = mQuery('#dwc_content');
|
||||
if (typeField === 'html') {
|
||||
Mautic.ConvertFieldToCkeditor(contentField, MauticVars.maxButtons);
|
||||
} else if (typeField === 'text') {
|
||||
if (ckEditors.size > 0) {
|
||||
ckEditors.forEach(function(value, key, map){
|
||||
map.get(key).destroy()
|
||||
})
|
||||
ckEditors.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.dynamicContentOnLoad = function (container, response) {
|
||||
if (typeof container !== 'object') {
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'dynamicContent');
|
||||
}
|
||||
}
|
||||
|
||||
var availableFilters = mQuery('div.dwc-filter').find('select[data-mautic="available_filters"]');
|
||||
Mautic.activateChosenSelect(availableFilters, false);
|
||||
|
||||
Mautic.dynamicFiltersOnLoad('div.dwc-filter');
|
||||
Mautic.dwcGenerator.init();
|
||||
};
|
||||
|
||||
Mautic.dynamicFiltersOnLoad = function(container, response) {
|
||||
|
||||
mQuery('#campaign-share-tab').hover(function () {
|
||||
if (Mautic.shareTableLoaded != true) {
|
||||
Mautic.loadAjaxColumn('campaign-share-stat', 'lead:getCampaignShareStats', 'afterStatsLoad');
|
||||
Mautic.shareTableLoaded = true;
|
||||
}
|
||||
})
|
||||
|
||||
Mautic.afterStatsLoad = function () {
|
||||
Mautic.sortTableByColumn('#campaign-share-table', '.campaign-share-stat', true)
|
||||
}
|
||||
|
||||
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'lead.list');
|
||||
}
|
||||
|
||||
var prefix = 'leadlist';
|
||||
var parent = mQuery('.dynamic-content-filter, .dwc-filter');
|
||||
if (parent.length) {
|
||||
prefix = parent.attr('id');
|
||||
}
|
||||
|
||||
if (mQuery('#' + prefix + '_filters').length) {
|
||||
mQuery('#available_filters').on('change', function() {
|
||||
if (mQuery(this).val()) {
|
||||
Mautic.addDwcFilter(mQuery(this).val(),mQuery('option:selected',this).data('field-object'));
|
||||
mQuery(this).val('');
|
||||
mQuery(this).trigger('chosen:updated');
|
||||
}
|
||||
});
|
||||
|
||||
mQuery('#' + prefix + '_filters .remove-selected').each( function (index, el) {
|
||||
mQuery(el).on('click', function () {
|
||||
mQuery(this).closest('.panel').animate(
|
||||
{'opacity': 0},
|
||||
'fast',
|
||||
function () {
|
||||
mQuery(this).remove();
|
||||
Mautic.reorderSegmentFilters();
|
||||
}
|
||||
);
|
||||
|
||||
if (!mQuery('#' + prefix + '_filters li:not(.placeholder)').length) {
|
||||
mQuery('#' + prefix + '_filters li.placeholder').removeClass('hide');
|
||||
} else {
|
||||
mQuery('#' + prefix + '_filters li.placeholder').addClass('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var bodyOverflow = {};
|
||||
mQuery('#' + prefix + '_filters').sortable({
|
||||
items: '.panel',
|
||||
helper: function(e, ui) {
|
||||
ui.children().each(function() {
|
||||
if (mQuery(this).is(":visible")) {
|
||||
mQuery(this).width(mQuery(this).width());
|
||||
}
|
||||
});
|
||||
|
||||
// Fix body overflow that messes sortable up
|
||||
bodyOverflow.overflowX = mQuery('body').css('overflow-x');
|
||||
bodyOverflow.overflowY = mQuery('body').css('overflow-y');
|
||||
mQuery('body').css({
|
||||
overflowX: 'visible',
|
||||
overflowY: 'visible'
|
||||
});
|
||||
|
||||
return ui;
|
||||
},
|
||||
scroll: true,
|
||||
axis: 'y',
|
||||
stop: function(e, ui) {
|
||||
// Restore original overflow
|
||||
mQuery('body').css(bodyOverflow);
|
||||
|
||||
// First in the list should be an "and"
|
||||
ui.item.find('select.glue-select').first().val('and');
|
||||
|
||||
Mautic.reorderSegmentFilters();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// segment contact filters
|
||||
var segmentContactForm = mQuery('#segment-contact-filters');
|
||||
|
||||
if (segmentContactForm.length) {
|
||||
segmentContactForm.on('change', function() {
|
||||
segmentContactForm.submit();
|
||||
}).on('keyup', function() {
|
||||
segmentContactForm.delay(200).submit();
|
||||
}).on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
Mautic.refreshSegmentContacts(segmentContactForm);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.addDwcFilter = function (elId, elObj) {
|
||||
var filterId = '#available_' + elObj + '_' + elId;
|
||||
var filterOption = mQuery(filterId);
|
||||
var label = filterOption.text();
|
||||
var alias = filterOption.val();
|
||||
|
||||
// Create a new filter
|
||||
|
||||
var filterNum = parseInt(mQuery('.available-filters').data('index'));
|
||||
mQuery('.available-filters').data('index', filterNum + 1);
|
||||
|
||||
var prototypeStr = mQuery('.available-filters').data('prototype');
|
||||
var fieldType = filterOption.data('field-type');
|
||||
var fieldObject = filterOption.data('field-object');
|
||||
var isSpecial = (mQuery.inArray(fieldType, ['leadlist', 'campaign', 'device_type', 'device_brand', 'device_os', 'lead_email_received', 'lead_email_sent', 'tags', 'multiselect', 'boolean', 'select', 'country', 'timezone', 'region', 'stage', 'locale', 'globalcategory']) != -1);
|
||||
|
||||
prototypeStr = prototypeStr.replace(/__name__/g, filterNum);
|
||||
prototypeStr = prototypeStr.replace(/__label__/g, label);
|
||||
|
||||
// Convert to DOM
|
||||
prototype = mQuery(prototypeStr);
|
||||
|
||||
var prefix = 'leadlist';
|
||||
var parent = mQuery(filterId).parents('.dynamic-content-filter, .dwc-filter');
|
||||
if (parent.length) {
|
||||
prefix = parent.attr('id');
|
||||
}
|
||||
|
||||
var filterBase = prefix + "[filters][" + filterNum + "]";
|
||||
var filterIdBase = prefix + "_filters_" + filterNum + "_";
|
||||
|
||||
if (isSpecial) {
|
||||
var templateField = fieldType;
|
||||
if (fieldType == 'boolean' || fieldType == 'multiselect') {
|
||||
templateField = 'select';
|
||||
}
|
||||
|
||||
var template = mQuery('#templates .' + templateField + '-template').clone();
|
||||
template.attr('name', mQuery(template).attr('name').replace(/__name__/g, filterNum));
|
||||
template.attr('id', mQuery(template).attr('id').replace(/__name__/g, filterNum));
|
||||
prototype.find('input[name="' + filterBase + '[filter]"]').replaceWith(template);
|
||||
}
|
||||
|
||||
if (mQuery('#' + prefix + '_filters div.panel').length == 0) {
|
||||
// First filter so hide the glue footer
|
||||
prototype.find(".panel-heading").addClass('hide');
|
||||
}
|
||||
|
||||
if (fieldObject == 'company') {
|
||||
prototype.find(".object-icon").removeClass('ri-user-6-fill').addClass('ri-building-2-line');
|
||||
} else {
|
||||
prototype.find(".object-icon").removeClass('ri-building-2-line').addClass('ri-user-6-fill');
|
||||
}
|
||||
prototype.find(".inline-spacer").append(fieldObject);
|
||||
|
||||
prototype.find("a.remove-selected").on('click', function() {
|
||||
mQuery(this).closest('.panel').animate(
|
||||
{'opacity': 0},
|
||||
'fast',
|
||||
function () {
|
||||
mQuery(this).remove();
|
||||
Mautic.reorderSegmentFilters();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
prototype.find("input[name='" + filterBase + "[field]']").val(elId);
|
||||
prototype.find("input[name='" + filterBase + "[type]']").val(fieldType);
|
||||
prototype.find("input[name='" + filterBase + "[object]']").val(fieldObject);
|
||||
|
||||
var filterEl = (isSpecial) ? "select[name='" + filterBase + "[filter]']" : "input[name='" + filterBase + "[filter]']";
|
||||
|
||||
prototype.appendTo('#' + prefix + '_filters');
|
||||
|
||||
var filter = mQuery('#' + filterIdBase + 'filter');
|
||||
|
||||
//activate fields
|
||||
if (isSpecial) {
|
||||
if (fieldType == 'select' || fieldType == 'multiselect' || fieldType == 'boolean' || fieldType == 'leadlist') {
|
||||
// Generate the options
|
||||
var fieldOptions = filterOption.data("field-list");
|
||||
mQuery.each(fieldOptions, function(val, index) {
|
||||
if (mQuery.isPlainObject(index)) {
|
||||
var optGroup = index;
|
||||
mQuery.each(optGroup, function(value, index) {
|
||||
mQuery('<option class="' + optGroup + '">').val(index).text(value).appendTo(filterEl);
|
||||
});
|
||||
mQuery('.' + index).wrapAll("<optgroup label='"+index+"' />");
|
||||
} else {
|
||||
mQuery('<option>').val(index).text(val).appendTo(filterEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (fieldType == 'lookup') {
|
||||
var fieldCallback = filterOption.data("field-callback");
|
||||
if (fieldCallback && typeof Mautic[fieldCallback] == 'function') {
|
||||
var fieldOptions = filterOption.data("field-list");
|
||||
Mautic[fieldCallback](filterIdBase + 'filter', elId, fieldOptions);
|
||||
} else {
|
||||
filter.attr('data-target', alias);
|
||||
Mautic.activateLookupTypeahead(filter.parent());
|
||||
}
|
||||
} else if (fieldType == 'datetime') {
|
||||
filter.datetimepicker({
|
||||
format: 'Y-m-d H:i',
|
||||
lazyInit: true,
|
||||
validateOnBlur: false,
|
||||
allowBlank: true,
|
||||
scrollMonth: false,
|
||||
scrollInput: false
|
||||
});
|
||||
} else if (fieldType == 'date') {
|
||||
filter.datetimepicker({
|
||||
timepicker: false,
|
||||
format: 'Y-m-d',
|
||||
lazyInit: true,
|
||||
validateOnBlur: false,
|
||||
allowBlank: true,
|
||||
scrollMonth: false,
|
||||
scrollInput: false,
|
||||
closeOnDateSelect: true
|
||||
});
|
||||
} else if (fieldType == 'time') {
|
||||
filter.datetimepicker({
|
||||
datepicker: false,
|
||||
format: 'H:i',
|
||||
lazyInit: true,
|
||||
validateOnBlur: false,
|
||||
allowBlank: true,
|
||||
scrollMonth: false,
|
||||
scrollInput: false
|
||||
});
|
||||
} else if (fieldType == 'lookup_id') {
|
||||
//switch the filter and display elements
|
||||
var oldFilter = mQuery(filterEl);
|
||||
var newDisplay = oldFilter.clone();
|
||||
newDisplay.attr('name', filterBase + '[display]')
|
||||
.attr('id', filterIdBase + 'display');
|
||||
|
||||
var oldDisplay = prototype.find("input[name='" + filterBase + "[display]']");
|
||||
var newFilter = mQuery(oldDisplay).clone();
|
||||
newFilter.attr('name', filterBase + '[filter]');
|
||||
newFilter.attr('id', filterIdBase + 'filter');
|
||||
|
||||
oldFilter.replaceWith(newFilter);
|
||||
oldDisplay.replaceWith(newDisplay);
|
||||
|
||||
var fieldCallback = filterOption.data("field-callback");
|
||||
if (fieldCallback && typeof Mautic[fieldCallback] == 'function') {
|
||||
var fieldOptions = filterOption.data("field-list");
|
||||
Mautic[fieldCallback](filterIdBase + 'display', elId, fieldOptions);
|
||||
}
|
||||
} else {
|
||||
filter.attr('type', fieldType);
|
||||
}
|
||||
|
||||
var operators = filterOption.data('field-operators');
|
||||
mQuery('#' + filterIdBase + 'operator').html('');
|
||||
mQuery.each(operators, function (label, value) {
|
||||
var newOption = mQuery('<option/>').val(value).text(label);
|
||||
newOption.appendTo(mQuery('#' + filterIdBase + 'operator'));
|
||||
});
|
||||
|
||||
// Convert based on first option in list
|
||||
Mautic.convertDwcFilterInput('#' + filterIdBase + 'operator');
|
||||
|
||||
// Reposition if applicable
|
||||
Mautic.updateFilterPositioning(mQuery('#' + filterIdBase + 'glue'));
|
||||
};
|
||||
|
||||
Mautic.convertDwcFilterInput = function(el) {
|
||||
var prefix = 'leadlist';
|
||||
|
||||
var parent = mQuery(el).parents('.dynamic-content-filter, .dwc-filter');
|
||||
if (parent.length) {
|
||||
prefix = parent.attr('id');
|
||||
}
|
||||
|
||||
var operator = mQuery(el).val();
|
||||
|
||||
// Extract the filter number
|
||||
var regExp = /_filters_(\d+)_operator/;
|
||||
var matches = regExp.exec(mQuery(el).attr('id'));
|
||||
var filterNum = matches[1];
|
||||
var filterId = '#' + prefix + '_filters_' + filterNum + '_filter';
|
||||
|
||||
// Reset has-error
|
||||
if (mQuery(filterId).parent().hasClass('has-error')) {
|
||||
mQuery(filterId).parent().find('div.help-block').hide();
|
||||
mQuery(filterId).parent().removeClass('has-error');
|
||||
}
|
||||
|
||||
var disabled = (operator == 'empty' || operator == '!empty');
|
||||
mQuery(filterId+', #' + prefix + '_filters_' + filterNum + '_display').prop('disabled', disabled);
|
||||
|
||||
if (disabled) {
|
||||
mQuery(filterId).val('');
|
||||
}
|
||||
|
||||
var newName = '';
|
||||
var lastPos;
|
||||
|
||||
if (mQuery(filterId).is('select')) {
|
||||
var isMultiple = mQuery(filterId).attr('multiple');
|
||||
var multiple = (operator == 'in' || operator == '!in');
|
||||
var placeholder = mQuery(filterId).attr('data-placeholder');
|
||||
|
||||
if (multiple && !isMultiple) {
|
||||
mQuery(filterId).attr('multiple', 'multiple');
|
||||
|
||||
// Update the name
|
||||
newName = mQuery(filterId).attr('name') + '[]';
|
||||
mQuery(filterId).attr('name', newName);
|
||||
|
||||
placeholder = mauticLang['chosenChooseMore'];
|
||||
} else if (!multiple && isMultiple) {
|
||||
mQuery(filterId).removeAttr('multiple');
|
||||
|
||||
// Update the name
|
||||
newName = mQuery(filterId).attr('name');
|
||||
lastPos = newName.lastIndexOf('[]');
|
||||
newName = newName.substring(0, lastPos);
|
||||
|
||||
mQuery(filterId).attr('name', newName);
|
||||
|
||||
placeholder = mauticLang['chosenChooseOne'];
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
// Remove empty option
|
||||
mQuery(filterId).find('option[value=""]').remove();
|
||||
|
||||
// Make sure none are selected
|
||||
mQuery(filterId + ' option:selected').removeAttr('selected');
|
||||
} else {
|
||||
// Add empty option
|
||||
mQuery(filterId).prepend("<option value='' selected></option>");
|
||||
}
|
||||
|
||||
// Destroy the chosen and recreate
|
||||
Mautic.destroyChosen(mQuery(filterId));
|
||||
|
||||
mQuery(filterId).attr('data-placeholder', placeholder);
|
||||
|
||||
Mautic.activateChosenSelect(mQuery(filterId));
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.standardDynamicContentUrl = function(options) {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = options.windowUrl;
|
||||
if (url) {
|
||||
var editDynamicContentKey = '/dwc/edit/dynamicContentId';
|
||||
var previewDynamicContentKey = '/dwc/preview/dynamicContentId';
|
||||
if (url.indexOf(editDynamicContentKey) > -1 ||
|
||||
url.indexOf(previewDynamicContentKey) > -1) {
|
||||
options.windowUrl = url.replace('dynamicContentId', mQuery('#campaignevent_properties_dynamicContent').val());
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
Mautic.disabledDynamicContentAction = function(opener) {
|
||||
if (typeof opener == 'undefined') {
|
||||
opener = window;
|
||||
}
|
||||
|
||||
var dynamicContent = opener.mQuery('#campaignevent_properties_dynamicContent').val();
|
||||
|
||||
var disabled = dynamicContent === '' || dynamicContent === null;
|
||||
|
||||
opener.mQuery('#campaignevent_properties_editDynamicContentButton').prop('disabled', disabled);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'items' => [
|
||||
'mautic.dynamicContent.dynamicContent' => [
|
||||
'route' => 'mautic_dynamicContent_index',
|
||||
'access' => ['dynamiccontent:dynamiccontents:viewown', 'dynamiccontent:dynamiccontents:viewother'],
|
||||
'parent' => 'mautic.core.components',
|
||||
'priority' => 90,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_dynamicContent_index' => [
|
||||
'path' => '/dwc/{page}',
|
||||
'controller' => 'Mautic\DynamicContentBundle\Controller\DynamicContentController::indexAction',
|
||||
],
|
||||
'mautic_dynamicContent_action' => [
|
||||
'path' => '/dwc/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\DynamicContentBundle\Controller\DynamicContentController::executeAction',
|
||||
],
|
||||
],
|
||||
'public' => [
|
||||
'mautic_api_dynamicContent_index' => [
|
||||
'path' => '/dwc',
|
||||
'controller' => 'Mautic\DynamicContentBundle\Controller\DynamicContentApiController::getAction',
|
||||
],
|
||||
'mautic_api_dynamicContent_action' => [
|
||||
'path' => '/dwc/{objectAlias}',
|
||||
'controller' => 'Mautic\DynamicContentBundle\Controller\DynamicContentApiController::processAction',
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_dynamicContent_standard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'dynamicContents',
|
||||
'path' => '/dynamiccontents',
|
||||
'controller' => Mautic\DynamicContentBundle\Controller\Api\DynamicContentApiController::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
'services' => [
|
||||
'forms' => [
|
||||
'mautic.form.type.dwc_entry_filters' => [
|
||||
'class' => Mautic\DynamicContentBundle\Form\Type\DwcEntryFiltersType::class,
|
||||
'arguments' => [
|
||||
'translator',
|
||||
'mautic.lead.model.list',
|
||||
],
|
||||
'methodCalls' => [
|
||||
'setConnection' => [
|
||||
'database_connection',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'mautic.helper.dynamicContent' => [
|
||||
'class' => Mautic\DynamicContentBundle\Helper\DynamicContentHelper::class,
|
||||
'arguments' => [
|
||||
'mautic.dynamicContent.model.dynamicContent',
|
||||
'mautic.campaign.executioner.realtime',
|
||||
'event_dispatcher',
|
||||
'mautic.lead.model.lead',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\DynamicContentBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\DynamicContentBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
$services->alias('mautic.dynamicContent.model.dynamicContent', Mautic\DynamicContentBundle\Model\DynamicContentModel::class);
|
||||
$services->alias('mautic.dynamicContent.repository.stat', Mautic\DynamicContentBundle\Entity\StatRepository::class);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use AjaxLookupControllerTrait;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<DynamicContent>
|
||||
*/
|
||||
class DynamicContentApiController extends CommonApiController
|
||||
{
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$dynamicContentModel = $modelFactory->getModel('dynamicContent');
|
||||
\assert($dynamicContentModel instanceof DynamicContentModel);
|
||||
|
||||
$this->model = $dynamicContentModel;
|
||||
$this->entityClass = DynamicContent::class;
|
||||
$this->entityNameOne = 'dynamicContent';
|
||||
$this->entityNameMulti = 'dynamicContents';
|
||||
$this->serializerGroups = ['dwcDetails', 'categoryList'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
|
||||
use Mautic\LeadBundle\Helper\ContactRequestHelper;
|
||||
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class DynamicContentApiController extends CommonController
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function processAction(Request $request, $objectAlias)
|
||||
{
|
||||
// Don't store a visitor with this request
|
||||
defined('MAUTIC_NON_TRACKABLE_REQUEST') || define('MAUTIC_NON_TRACKABLE_REQUEST', 1);
|
||||
|
||||
$method = strtolower($request->getMethod());
|
||||
if (method_exists($this, $method.'Action')) {
|
||||
return $this->forwardWithPost(
|
||||
static::class.'::'.$method.'Action',
|
||||
$request->request->all(),
|
||||
[
|
||||
'objectAlias' => $objectAlias,
|
||||
],
|
||||
$request->query->all()
|
||||
);
|
||||
} else {
|
||||
throw new HttpException(Response::HTTP_FORBIDDEN, 'This endpoint is not able to process '.strtoupper($method).' requests.');
|
||||
}
|
||||
}
|
||||
|
||||
public function getAction(
|
||||
Request $request,
|
||||
DynamicContentHelper $helper,
|
||||
DeviceTrackingServiceInterface $deviceTrackingService,
|
||||
ContactRequestHelper $contactRequestHelper,
|
||||
$objectAlias,
|
||||
): Response {
|
||||
/** @var PageModel $pageModel */
|
||||
$pageModel = $this->getModel('page');
|
||||
|
||||
$lead = $contactRequestHelper->getContactFromQuery($pageModel->getHitQuery($request));
|
||||
$content = $helper->getDynamicContentForLead($objectAlias, $lead);
|
||||
$trackedDevice = $deviceTrackingService->getTrackedDevice();
|
||||
$deviceId = (null === $trackedDevice ? null : $trackedDevice->getTrackingId());
|
||||
|
||||
return empty($content)
|
||||
? new Response('', Response::HTTP_NO_CONTENT)
|
||||
: new JsonResponse(
|
||||
[
|
||||
'content' => $content,
|
||||
'id' => $lead->getId(),
|
||||
'sid' => $deviceId,
|
||||
'device_id' => $deviceId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Form\Type\DateRangeType;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DynamicContentController extends FormController
|
||||
{
|
||||
protected function getPermissions(): array
|
||||
{
|
||||
return (array) $this->security->isGranted(
|
||||
[
|
||||
'dynamiccontent:dynamiccontents:viewown',
|
||||
'dynamiccontent:dynamiccontents:viewother',
|
||||
'dynamiccontent:dynamiccontents:create',
|
||||
'dynamiccontent:dynamiccontents:editown',
|
||||
'dynamiccontent:dynamiccontents:editother',
|
||||
'dynamiccontent:dynamiccontents:deleteown',
|
||||
'dynamiccontent:dynamiccontents:deleteother',
|
||||
'dynamiccontent:dynamiccontents:publishown',
|
||||
'dynamiccontent:dynamiccontents:publishother',
|
||||
],
|
||||
'RETURN_ARRAY'
|
||||
);
|
||||
}
|
||||
|
||||
public function indexAction(Request $request, $page = 1)
|
||||
{
|
||||
$model = $this->getModel('dynamicContent');
|
||||
|
||||
$permissions = $this->getPermissions();
|
||||
|
||||
if (!$permissions['dynamiccontent:dynamiccontents:viewown'] && !$permissions['dynamiccontent:dynamiccontents:viewother']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$limit = $request->getSession()->get('mautic.dynamicContent.limit', $this->coreParametersHelper->get('default_pagelimit'));
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
// fetch
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.dynamicContent.filter', ''));
|
||||
$request->getSession()->set('mautic.dynamicContent.filter', $search);
|
||||
|
||||
$filter = [
|
||||
'string' => $search,
|
||||
'force' => [
|
||||
['column' => 'e.variantParent', 'expr' => 'isNull'],
|
||||
['column' => 'e.translationParent', 'expr' => 'isNull'],
|
||||
],
|
||||
];
|
||||
|
||||
$orderBy = $request->getSession()->get('mautic.dynamicContent.orderby', 'e.name');
|
||||
$orderByDir = $request->getSession()->get('mautic.dynamicContent.orderbydir', 'DESC');
|
||||
|
||||
$entities = $model->getEntities(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]
|
||||
);
|
||||
|
||||
// set what page currently on so that we can return here after form submission/cancellation
|
||||
$request->getSession()->set('mautic.dynamicContent.page', $page);
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
|
||||
// retrieve a list of categories
|
||||
$pageModel = $this->getModel('page');
|
||||
\assert($pageModel instanceof PageModel);
|
||||
$categories = $pageModel->getLookupResults('category', '', 0);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'contentTemplate' => '@MauticDynamicContent/DynamicContent/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
'route' => $this->generateUrl('mautic_dynamicContent_index', ['page' => $page]),
|
||||
],
|
||||
'viewParameters' => [
|
||||
'searchValue' => $search,
|
||||
'items' => $entities,
|
||||
'categories' => $categories,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'permissions' => $permissions,
|
||||
'model' => $model,
|
||||
'tmpl' => $tmpl,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function newAction(Request $request, $entity = null)
|
||||
{
|
||||
if (!$this->security->isGranted('dynamiccontent:dynamiccontents:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
if (!$entity instanceof DynamicContent) {
|
||||
$entity = new DynamicContent();
|
||||
}
|
||||
|
||||
$model = $this->getModel('dynamicContent');
|
||||
\assert($model instanceof DynamicContentModel);
|
||||
$method = $request->getMethod();
|
||||
$page = $request->getSession()->get('mautic.dynamicContent.page', 1);
|
||||
$retUrl = $this->generateUrl('mautic_dynamicContent_index', ['page' => $page]);
|
||||
$action = $this->generateUrl('mautic_dynamicContent_action', ['objectAction' => 'new']);
|
||||
$dwc = $request->request->all()['dwc'] ?? [];
|
||||
$updateSelect = 'POST' === $method
|
||||
? ($dwc['updateSelect'] ?? false)
|
||||
: $request->get('updateSelect', false);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['update_select' => $updateSelect]);
|
||||
|
||||
if (Request::METHOD_POST === $method) {
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$model->saveEntity($entity);
|
||||
|
||||
$this->addFlashMessage(
|
||||
'mautic.core.notice.created',
|
||||
[
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_dynamicContent_index',
|
||||
'%url%' => $this->generateUrl(
|
||||
'mautic_dynamicContent_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
if ($this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
$viewParameters = [
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $entity->getId(),
|
||||
];
|
||||
$retUrl = $this->generateUrl('mautic_dynamicContent_action', $viewParameters);
|
||||
$template = 'Mautic\DynamicContentBundle\Controller\DynamicContentController::viewAction';
|
||||
} else {
|
||||
// return edit view so that all the session stuff is loaded
|
||||
return $this->editAction($request, $entity->getId(), true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$viewParameters = ['page' => $page];
|
||||
$retUrl = $this->generateUrl('mautic_dynamicContent_index', $viewParameters);
|
||||
$template = 'Mautic\DynamicContentBundle\Controller\DynamicContentController::indexAction';
|
||||
}
|
||||
|
||||
$passthrough = [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
];
|
||||
|
||||
// Check to see if this is a popup
|
||||
if (isset($form['updateSelect'])) {
|
||||
$template = false;
|
||||
$passthrough = array_merge(
|
||||
$passthrough,
|
||||
[
|
||||
'updateSelect' => $form['updateSelect']->getData(),
|
||||
'id' => $entity->getId(),
|
||||
'name' => $entity->getName(),
|
||||
'group' => $entity->getLanguage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $retUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
'passthroughVars' => $passthrough,
|
||||
]
|
||||
);
|
||||
} elseif ($valid && !$cancelled) {
|
||||
return $this->editAction($request, $entity->getId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
$passthrough['route'] = $action;
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticDynamicContent/DynamicContent/form.html.twig',
|
||||
'passthroughVars' => $passthrough,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's edit form and processes post data.
|
||||
*
|
||||
* @param bool|false $ignorePost
|
||||
*
|
||||
* @return array|JsonResponse|RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
/** @var DynamicContentModel $model */
|
||||
$model = $this->getModel('dynamicContent');
|
||||
$entity = $model->getEntity($objectId);
|
||||
$page = $request->getSession()->get('mautic.dynamicContent.page', 1);
|
||||
$retUrl = $this->generateUrl('mautic_dynamicContent_index', ['page' => $page]);
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $retUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\DynamicContentBundle\Controller\DynamicContentController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
],
|
||||
];
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge(
|
||||
$postActionVars,
|
||||
[
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.dynamicContent.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
} elseif (!$this->security->hasEntityAccess(true, 'dynamiccontent:dynamiccontents:editother', $entity->getCreatedBy())) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $entity, 'dynamicContent');
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_dynamicContent_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$method = $request->getMethod();
|
||||
$dwc = $request->request->all()['dwc'] ?? [];
|
||||
$updateSelect = 'POST' === $method
|
||||
? ($dwc['updateSelect'] ?? false)
|
||||
: $request->get('updateSelect', false);
|
||||
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['update_select' => $updateSelect]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $method) {
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
|
||||
$this->addFlashMessage(
|
||||
'mautic.core.notice.updated',
|
||||
[
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_dynamicContent_index',
|
||||
'%url%' => $this->generateUrl(
|
||||
'mautic_dynamicContent_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($entity);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->viewAction($request, $entity->getId());
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($entity);
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'currentListId' => $objectId,
|
||||
],
|
||||
'contentTemplate' => '@MauticDynamicContent/DynamicContent/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'route' => $action,
|
||||
'mauticContent' => 'dynamicContent',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific form into the detailed panel.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, $objectId)
|
||||
{
|
||||
$model = $this->getModel('dynamicContent');
|
||||
\assert($model instanceof DynamicContentModel);
|
||||
$security = $this->security;
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.dynamicContent.page', 1);
|
||||
|
||||
if (null === $entity) {
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_dynamicContent_index', ['page' => $page]);
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\DynamicContentBundle\Controller\DynamicContentController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
],
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.dynamicContent.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
} elseif (!$security->hasEntityAccess(
|
||||
'dynamiccontent:dynamiccontents:viewown',
|
||||
'dynamiccontent:dynamiccontents:viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/* @var DynamicContent $parent */
|
||||
/* @var DynamicContent[] $children */
|
||||
[$translationParent, $translationChildren] = $entity->getTranslations();
|
||||
|
||||
// Audit Log
|
||||
$auditLogModel = $this->getModel('core.auditlog');
|
||||
\assert($auditLogModel instanceof AuditLogModel);
|
||||
$logs = $auditLogModel->getLogForObject('dynamicContent', $entity->getId(), $entity->getDateAdded());
|
||||
|
||||
// Init the date range filter form
|
||||
$dateRangeValues = $request->query->all()['daterange'] ?? $request->request->all()['daterange'] ?? [];
|
||||
$action = $this->generateUrl('mautic_dynamicContent_action', ['objectAction' => 'view', 'objectId' => $objectId]);
|
||||
$dateRangeForm = $this->formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
|
||||
$entityViews = $model->getHitsLineChartData(
|
||||
null,
|
||||
new \DateTime($dateRangeForm->get('date_from')->getData()),
|
||||
new \DateTime($dateRangeForm->get('date_to')->getData()),
|
||||
null,
|
||||
['dynamic_content_id' => $entity->getId(), 'flag' => 'total_and_unique']
|
||||
);
|
||||
|
||||
$trackableModel = $this->getModel('page.trackable');
|
||||
\assert($trackableModel instanceof TrackableModel);
|
||||
$trackables = $trackableModel->getTrackableList('dynamicContent', $entity->getId());
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'returnUrl' => $action,
|
||||
'contentTemplate' => '@MauticDynamicContent/DynamicContent/details.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
],
|
||||
'viewParameters' => [
|
||||
'entity' => $entity,
|
||||
'permissions' => $this->getPermissions(),
|
||||
'logs' => $logs,
|
||||
'isEmbedded' => $request->get('isEmbedded') ?: false,
|
||||
'translations' => [
|
||||
'parent' => $translationParent,
|
||||
'children' => $translationChildren,
|
||||
],
|
||||
'trackables' => $trackables,
|
||||
'entityViews' => $entityViews,
|
||||
'dateRangeForm' => $dateRangeForm->createView(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function cloneAction(Request $request, $objectId)
|
||||
{
|
||||
$model = $this->getModel('dynamicContent');
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null != $entity) {
|
||||
if (!$this->security->isGranted('dynamiccontent:dynamiccontents:create')
|
||||
|| !$this->security->hasEntityAccess(
|
||||
'dynamiccontent:dynamiccontents:viewown',
|
||||
'dynamiccontent:dynamiccontents:viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$entity = clone $entity;
|
||||
}
|
||||
|
||||
return $this->newAction($request, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.dynamicContent.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_dynamicContent_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\DynamicContentBundle\Controller\DynamicContentController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('dynamicContent');
|
||||
\assert($model instanceof DynamicContentModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.dynamicContent.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
|
||||
return $this->postActionRedirect(array_merge($postActionVars, ['flashes' => $flashes]));
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'dynamiccontent:dynamiccontents:deleteown',
|
||||
'dynamiccontent:dynamiccontents:deleteother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'notification');
|
||||
}
|
||||
|
||||
$model->deleteEntity($entity);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $entity->getName(),
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(array_merge($postActionVars, ['flashes' => $flashes]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*/
|
||||
public function batchDeleteAction(Request $request): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.dynamicContent.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_dynamicContent_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\DynamicContentBundle\Controller\DynamicContentController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_dynamicContent_index',
|
||||
'mauticContent' => 'dynamicContent',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('dynamicContent');
|
||||
\assert($model instanceof DynamicContentModel);
|
||||
$ids = json_decode($request->query->get('ids', '{}'));
|
||||
|
||||
$deleteIds = [];
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.dynamicContent.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'dynamiccontent:dynamiccontents:viewown',
|
||||
'dynamiccontent:dynamiccontents:viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'dynamicContent', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.dynamicContent.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(array_merge($postActionVars, ['flashes' => $flashes]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticDynamicContentExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\DynamicContent;
|
||||
|
||||
final class TypeList
|
||||
{
|
||||
public const HTML = 'html';
|
||||
public const TEXT = 'text';
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getChoices(): array
|
||||
{
|
||||
return [
|
||||
'mautic.dynamic.content.type.html' => self::HTML,
|
||||
'mautic.dynamic.content.type.text' => self::TEXT,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle;
|
||||
|
||||
/**
|
||||
* Events available for DynamicContentBundle.
|
||||
*/
|
||||
final class DynamicContentEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.dwc_token_replacement event is thrown right before the content is returned.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CoreBundle\Event\TokenReplacementEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TOKEN_REPLACEMENT = 'mautic.dwc_token_replacement';
|
||||
|
||||
/**
|
||||
* The mautic.dwc_pre_save event is thrown right before a asset is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DynamicContentBundle\Event\DynamicContentEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PRE_SAVE = 'mautic.dwc_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.dwc_post_save event is thrown right after a asset is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DynamicContentBundle\Event\DynamicContentEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POST_SAVE = 'mautic.dwc_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.dwc_pre_delete event is thrown prior to when a asset is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DynamicContentBundle\Event\DynamicContentEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PRE_DELETE = 'mautic.dwc_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.dwc_post_delete event is thrown after a asset is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DynamicContentBundle\Event\DynamicContentEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POST_DELETE = 'mautic.dwc_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.category_pre_save event is thrown right before a category is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CategoryBundle\Event\CategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CATEGORY_PRE_SAVE = 'mautic.category_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.category_post_save event is thrown right after a category is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CategoryBundle\Event\CategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CATEGORY_POST_SAVE = 'mautic.category_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.category_pre_delete event is thrown prior to when a category is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CategoryBundle\Event\CategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CATEGORY_PRE_DELETE = 'mautic.category_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.category_post_delete event is thrown after a category is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CategoryBundle\Event\CategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CATEGORY_POST_DELETE = 'mautic.category_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.asset.on_campaign_trigger_decision event is fired when the campaign decision triggers.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_TRIGGER_DECISION = 'mautic.dwc.on_campaign_trigger_decision';
|
||||
|
||||
/**
|
||||
* The mautic.asset.on_campaign_trigger_action event is fired when the campaign action triggers.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.dwc.on_campaign_trigger_action';
|
||||
|
||||
/**
|
||||
* The mautic.dwc.on_contact_filters_evaluate event is fired when dynamic content's decision's
|
||||
* filters need to be evaluated.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\DynamicContentBundle\Event\ContactFiltersEvaluateEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CONTACTS_FILTER_EVALUATE = 'mautic.dwc.on_contact_filters_evaluate';
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FiltersEntityTrait;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\TranslationEntityInterface;
|
||||
use Mautic\CoreBundle\Entity\TranslationEntityTrait;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\CoreBundle\Entity\VariantEntityInterface;
|
||||
use Mautic\CoreBundle\Entity\VariantEntityTrait;
|
||||
use Mautic\DynamicContentBundle\DynamicContent\TypeList;
|
||||
use Mautic\DynamicContentBundle\Validator\Constraints\NoNesting;
|
||||
use Mautic\DynamicContentBundle\Validator\Constraints\SlotNameType;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\Choice;
|
||||
use Symfony\Component\Validator\Constraints\Count;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('dynamiccontent:dynamiccontents:viewown')"),
|
||||
new Post(security: "is_granted('dynamiccontent:dynamiccontents:create')"),
|
||||
new Get(security: "is_granted('dynamiccontent:dynamiccontents:viewown')"),
|
||||
new Put(security: "is_granted('dynamiccontent:dynamiccontents:editown')"),
|
||||
new Patch(security: "is_granted('dynamiccontent:dynamiccontents:editother')"),
|
||||
new Delete(security: "is_granted('dynamiccontent:dynamiccontents:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['dynamicContent:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category', 'translationChildren'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['dynamicContent:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
/**
|
||||
* @use TranslationEntityTrait<DynamicContent>
|
||||
* @use VariantEntityTrait<DynamicContent>
|
||||
*/
|
||||
class DynamicContent extends FormEntity implements VariantEntityInterface, TranslationEntityInterface, UuidInterface
|
||||
{
|
||||
use TranslationEntityTrait;
|
||||
use VariantEntityTrait;
|
||||
use FiltersEntityTrait;
|
||||
use UuidTrait;
|
||||
use ProjectTrait;
|
||||
|
||||
public const ENTITY_NAME = 'dynamic_content';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['dynamicContent:read'])]
|
||||
private $id;
|
||||
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private string $type = TypeList::HTML;
|
||||
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private ?string $description = null;
|
||||
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private ?Category $category = null;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private $publishDown;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private $utmTags = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['dynamicContent:read'])]
|
||||
private $sentCount = 0;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<Stat>
|
||||
*/
|
||||
#[Groups(['dynamicContent:read'])]
|
||||
private $stats;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private $isCampaignBased = true;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['dynamicContent:read', 'dynamicContent:write'])]
|
||||
private $slotName;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->stats = new ArrayCollection();
|
||||
$this->translationChildren = new ArrayCollection();
|
||||
$this->variantChildren = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
$this->sentCount = 0;
|
||||
$this->stats = new ArrayCollection();
|
||||
$this->translationChildren = new ArrayCollection();
|
||||
$this->variantChildren = new ArrayCollection();
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function clearStats(): void
|
||||
{
|
||||
$this->stats = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('dynamic_content')
|
||||
->addIndex(['is_campaign_based'], 'is_campaign_based_index')
|
||||
->addIndex(['slot_name'], 'slot_name_index')
|
||||
->setCustomRepositoryClass(DynamicContentRepository::class)
|
||||
->addLifecycleEvent('cleanSlotName', Events::prePersist)
|
||||
->addLifecycleEvent('cleanSlotName', Events::preUpdate);
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->addField(
|
||||
'type',
|
||||
Types::STRING,
|
||||
[
|
||||
'length' => 10,
|
||||
'default' => TypeList::HTML,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->addPublishDates();
|
||||
|
||||
$builder->createField('sentCount', 'integer')
|
||||
->columnName('sent_count')
|
||||
->build();
|
||||
|
||||
$builder->createField('content', 'text')
|
||||
->columnName('content')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('utmTags', Types::JSON)
|
||||
->columnName('utm_tags')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('stats', 'Stat')
|
||||
->setIndexBy('id')
|
||||
->mappedBy('dynamicContent')
|
||||
->cascadePersist()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
self::addTranslationMetadata($builder, self::class);
|
||||
self::addVariantMetadata($builder, self::class);
|
||||
self::addFiltersMetadata($builder);
|
||||
|
||||
$builder->createField('isCampaignBased', 'boolean')
|
||||
->columnName('is_campaign_based')
|
||||
->option('default', 1)
|
||||
->build();
|
||||
|
||||
$builder->createField('slotName', 'string')
|
||||
->columnName('slot_name')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'dynamic_content_projects_xref', 'dynamic_content_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException
|
||||
* @throws \Symfony\Component\Validator\Exception\InvalidOptionsException
|
||||
* @throws \Symfony\Component\Validator\Exception\MissingOptionsException
|
||||
*/
|
||||
public static function loadValidatorMetaData(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new NotBlank(['message' => 'mautic.core.name.required']));
|
||||
$metadata->addPropertyConstraint('content', new NoNesting());
|
||||
|
||||
$metadata->addPropertyConstraint('type', new NotBlank(['message' => 'mautic.core.type.required']));
|
||||
$metadata->addPropertyConstraint('type', new Choice(['choices' => (new TypeList())->getChoices()]));
|
||||
|
||||
$metadata->addConstraint(new SlotNameType());
|
||||
|
||||
$metadata->addConstraint(new Callback(
|
||||
function (self $dwc, ExecutionContextInterface $context): void {
|
||||
if (!$dwc->getIsCampaignBased()) {
|
||||
$validator = $context->getValidator();
|
||||
$violations = $validator->validate(
|
||||
$dwc->getSlotName(),
|
||||
[
|
||||
new NotBlank(
|
||||
[
|
||||
'message' => 'mautic.dynamicContent.slot_name.notblank',
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
foreach ($violations as $violation) {
|
||||
$context->buildViolation($violation->getMessage())
|
||||
->atPath('slotName')
|
||||
->addViolation();
|
||||
}
|
||||
$violations = $validator->validate(
|
||||
$dwc->getFilters(),
|
||||
[
|
||||
new Count(
|
||||
[
|
||||
'minMessage' => 'mautic.dynamicContent.filter.options.empty',
|
||||
'min' => 1,
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
foreach ($violations as $violation) {
|
||||
$context->buildViolation($violation->getMessage())
|
||||
->atPath('filters')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('dwc')
|
||||
->addListProperties([
|
||||
'id',
|
||||
'name',
|
||||
'category',
|
||||
'type',
|
||||
])
|
||||
->addProperties([
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'sentCount',
|
||||
'variantParent',
|
||||
'variantChildren',
|
||||
'content',
|
||||
'utmTags',
|
||||
'filters',
|
||||
'isCampaignBased',
|
||||
'slotName',
|
||||
])
|
||||
->setMaxDepth(1, 'variantParent')
|
||||
->setMaxDepth(1, 'variantChildren')
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'dwc');
|
||||
}
|
||||
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$getter = 'get'.ucfirst($prop);
|
||||
$current = $this->$getter();
|
||||
|
||||
if ('variantParent' == $prop || 'translationParent' == $prop || 'category' == $prop) {
|
||||
$currentId = ($current) ? $current->getId() : '';
|
||||
$newId = ($val) ? $val->getId() : null;
|
||||
if ($currentId != $newId) {
|
||||
$this->changes[$prop] = [$currentId, $newId];
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(string $type): void
|
||||
{
|
||||
$type = strtolower($type);
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Category
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Category $category
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCategory($category)
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $publishUp
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $publishDown
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setContent($content)
|
||||
{
|
||||
$this->isChanged('content', $content);
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $includeVariants
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSentCount($includeVariants = false)
|
||||
{
|
||||
return $includeVariants ? $this->getAccumulativeTranslationCount('getSentCount') : $this->sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSentCount($sentCount)
|
||||
{
|
||||
$this->sentCount = $sentCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
return $this->stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsCampaignBased()
|
||||
{
|
||||
return $this->isCampaignBased;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isCampaignBased
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setIsCampaignBased($isCampaignBased)
|
||||
{
|
||||
$this->isChanged('isCampaignBased', $isCampaignBased);
|
||||
$this->isCampaignBased = $isCampaignBased;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSlotName()
|
||||
{
|
||||
return $this->slotName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slotName
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSlotName($slotName)
|
||||
{
|
||||
$this->isChanged('slotName', $slotName);
|
||||
$this->slotName = $slotName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle callback to clear the slot name if is_campaign is true.
|
||||
*/
|
||||
public function cleanSlotName(): void
|
||||
{
|
||||
if ($this->getIsCampaignBased()) {
|
||||
$this->setSlotName('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DynamicContent
|
||||
*/
|
||||
public function setUtmTags(array $utmTags)
|
||||
{
|
||||
$this->isChanged('utmTags', $utmTags);
|
||||
$this->utmTags = $utmTags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getUtmTags()
|
||||
{
|
||||
return $this->utmTags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonEntity;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class DynamicContentLeadData extends CommonEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateAdded;
|
||||
|
||||
/**
|
||||
* @var DynamicContent|null
|
||||
*/
|
||||
private $dynamicContent;
|
||||
|
||||
/**
|
||||
* @var Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dataAdded;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $slot;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('dynamic_content_lead_data')
|
||||
->setCustomRepositoryClass(DynamicContentLeadDataRepository::class);
|
||||
|
||||
$builder->addIdColumns(false, false);
|
||||
|
||||
$builder->addDateAdded(true);
|
||||
|
||||
$builder->addLead();
|
||||
|
||||
$builder->createManyToOne('dynamicContent', 'DynamicContent')
|
||||
->inversedBy('id')
|
||||
->addJoinColumn('dynamic_content_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createField('slot', 'text')
|
||||
->columnName('slot')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateAdded()
|
||||
{
|
||||
return $this->dateAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateAdded
|
||||
*
|
||||
* @return DynamicContentLeadData
|
||||
*/
|
||||
public function setDateAdded($dateAdded)
|
||||
{
|
||||
$this->dateAdded = $dateAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DynamicContent
|
||||
*/
|
||||
public function getDynamicContent()
|
||||
{
|
||||
return $this->dynamicContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DynamicContent $dynamicContent
|
||||
*
|
||||
* @return DynamicContentLeadData
|
||||
*/
|
||||
public function setDynamicContent($dynamicContent)
|
||||
{
|
||||
$this->dynamicContent = $dynamicContent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
*
|
||||
* @return DynamicContentLeadData
|
||||
*/
|
||||
public function setLead($lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDataAdded()
|
||||
{
|
||||
return $this->dataAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dataAdded
|
||||
*
|
||||
* @return DynamicContentLeadData
|
||||
*/
|
||||
public function setDataAdded($dataAdded)
|
||||
{
|
||||
$this->dataAdded = $dataAdded;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSlot()
|
||||
{
|
||||
return $this->slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slot
|
||||
*
|
||||
* @return DynamicContentLeadData
|
||||
*/
|
||||
public function setSlot($slot)
|
||||
{
|
||||
$this->slot = $slot;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<DynamicContentLeadData>
|
||||
*/
|
||||
class DynamicContentLeadDataRepository extends CommonRepository
|
||||
{
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'dcld';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Serializer;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<DynamicContent>
|
||||
*/
|
||||
class DynamicContentRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* Get a list of entities.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->_em
|
||||
->createQueryBuilder()
|
||||
->select('e')
|
||||
->from(DynamicContent::class, 'e', 'e.id');
|
||||
|
||||
if (empty($args['iterable_mode'])) {
|
||||
$q->leftJoin('e.category', 'c');
|
||||
}
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
[$expr, $parameters] = parent::addSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.core.searchcommand.lang'):
|
||||
$langUnique = $this->generateRandomParameterName();
|
||||
$langValue = $filter->string.'_%';
|
||||
$forceParameters = [
|
||||
$langUnique => $langValue,
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = $q->expr()->or(
|
||||
$q->expr()->eq('e.language', ":$unique"),
|
||||
$q->expr()->like('e.language', ":$langUnique")
|
||||
);
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'dynamic_content_id',
|
||||
'dynamic_content_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.isuncategorized',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.core.searchcommand.lang',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['e.name', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'e';
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the sent counts.
|
||||
*
|
||||
* @param int $increaseBy
|
||||
*/
|
||||
public function upSentCount($id, $increaseBy = 1): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'dynamic_content')
|
||||
->set('sent_count', 'sent_count + '.(int) $increaseBy)
|
||||
->where('id = '.(int) $id);
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param bool $viewOther
|
||||
* @param bool $topLevel
|
||||
* @param array $ignoreIds
|
||||
* @param string $where
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDynamicContentList($search = '', $limit = 10, $start = 0, $viewOther = false, $topLevel = false, $ignoreIds = [], $where = null)
|
||||
{
|
||||
$q = $this->createQueryBuilder('e');
|
||||
$q->select('partial e.{id, name, language}');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (is_array($search)) {
|
||||
$search = array_map('intval', $search);
|
||||
$q->andWhere($q->expr()->in('e.id', ':search'))
|
||||
->setParameter('search', $search);
|
||||
} else {
|
||||
$q->andWhere($q->expr()->like('e.name', ':search'))
|
||||
->setParameter('search', "%{$search}%");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$viewOther) {
|
||||
$q->andWhere($q->expr()->eq('e.createdBy', ':id'))
|
||||
->setParameter('id', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
if ('translation' == $topLevel) {
|
||||
// only get top level pages
|
||||
$q->andWhere($q->expr()->isNull('e.translationParent'));
|
||||
} elseif ('variant' == $topLevel) {
|
||||
$q->andWhere($q->expr()->isNull('e.variantParent'));
|
||||
}
|
||||
|
||||
if (!empty($ignoreIds)) {
|
||||
$q->andWhere($q->expr()->notIn('e.id', ':dwc_ids'))
|
||||
->setParameter('dwc_ids', $ignoreIds);
|
||||
}
|
||||
|
||||
if ($where) {
|
||||
$q->andWhere($where);
|
||||
}
|
||||
|
||||
$q->orderBy('e.name');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|object|null
|
||||
*/
|
||||
public function getDynamicContentForSlotFromCampaign($slot)
|
||||
{
|
||||
$qb = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$qb->select('ce.properties')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_events', 'ce')
|
||||
->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = ce.campaign_id')
|
||||
->andWhere($qb->expr()->eq('ce.type', $qb->expr()->literal('dwc.decision')))
|
||||
->andWhere($qb->expr()->like('ce.properties', ':slot'))
|
||||
->setParameter('slot', '%'.$slot.'%')
|
||||
->orderBy('c.is_published');
|
||||
|
||||
$result = $qb->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($result as $item) {
|
||||
$properties = Serializer::decode($item['properties']);
|
||||
|
||||
if (isset($properties['dynamicContent'])) {
|
||||
$dwc = $this->getEntity($properties['dynamicContent']);
|
||||
|
||||
if ($dwc instanceof DynamicContent) {
|
||||
return $dwc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class Stat
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var DynamicContent|null
|
||||
*/
|
||||
private $dynamicContent;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateSent;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sentCount;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $lastSent;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $sentDetails = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sourceId;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $tokens = [];
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('dynamic_content_stats')
|
||||
->setCustomRepositoryClass(StatRepository::class)
|
||||
->addIndex(['dynamic_content_id', 'lead_id'], 'stat_dynamic_content_search')
|
||||
->addIndex(['source', 'source_id'], 'stat_dynamic_content_source_search')
|
||||
->addIndex(['date_sent'], 'stat_dynamic_content_date_sent');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createManyToOne('dynamicContent', 'DynamicContent')
|
||||
->inversedBy('stats')
|
||||
->addJoinColumn('dynamic_content_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addLead(true, 'SET NULL');
|
||||
|
||||
$builder->createField('dateSent', 'datetime')
|
||||
->columnName('date_sent')
|
||||
->build();
|
||||
|
||||
$builder->createField('source', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('sourceId', 'integer')
|
||||
->columnName('source_id')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('tokens', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('sentCount', 'integer', 'sent_count');
|
||||
|
||||
$builder->addNullableField('lastSent', 'datetime', 'last_sent');
|
||||
|
||||
$builder->addNullableField('sentDetails', 'array', 'sent_details');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('stat')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'dateSent',
|
||||
'source',
|
||||
'sentCount',
|
||||
'lastSent',
|
||||
'sourceId',
|
||||
'lead',
|
||||
'dynamicContent',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function addSentDetails($details): void
|
||||
{
|
||||
$this->sentDetails[] = $details;
|
||||
|
||||
++$this->sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the sent count.
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function upSentCount()
|
||||
{
|
||||
$count = (int) $this->sentCount + 1;
|
||||
$this->sentCount = $count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*/
|
||||
public function setId($id): void
|
||||
{
|
||||
$this->id = (string) $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DynamicContent
|
||||
*/
|
||||
public function getDynamicContent()
|
||||
{
|
||||
return $this->dynamicContent;
|
||||
}
|
||||
|
||||
public function setDynamicContent(DynamicContent $dynamicContent): void
|
||||
{
|
||||
$this->dynamicContent = $dynamicContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateSent()
|
||||
{
|
||||
return $this->dateSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateSent
|
||||
*/
|
||||
public function setDateSent($dateSent): void
|
||||
{
|
||||
$this->dateSent = $dateSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSentCount()
|
||||
{
|
||||
return $this->sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $sentCount
|
||||
*/
|
||||
public function setSentCount($sentCount): void
|
||||
{
|
||||
$this->sentCount = $sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLastSent()
|
||||
{
|
||||
return $this->lastSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $lastSent
|
||||
*/
|
||||
public function setLastSent($lastSent): void
|
||||
{
|
||||
$this->lastSent = $lastSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSentDetails()
|
||||
{
|
||||
return $this->sentDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sentDetails
|
||||
*/
|
||||
public function setSentDetails($sentDetails): void
|
||||
{
|
||||
$this->sentDetails = $sentDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $source
|
||||
*/
|
||||
public function setSource($source): void
|
||||
{
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSourceId()
|
||||
{
|
||||
return $this->sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $sourceId
|
||||
*/
|
||||
public function setSourceId($sourceId): void
|
||||
{
|
||||
$this->sourceId = $sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTokens()
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tokens
|
||||
*/
|
||||
public function setTokens($tokens): void
|
||||
{
|
||||
$this->tokens = $tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Stat>
|
||||
*/
|
||||
class StatRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
public function getSentStats($dynamicContentId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('s.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'dynamic_content_stats', 's')
|
||||
->where('s.dynamic_content_id = :dynamic_content')
|
||||
->setParameter('dynamic_content', $dynamicContentId);
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// index by lead
|
||||
$stats = [];
|
||||
|
||||
foreach ($result as $r) {
|
||||
$stats[$r['lead_id']] = $r['lead_id'];
|
||||
}
|
||||
|
||||
unset($result);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|array $dynamicContentIds
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSentCount($dynamicContentIds = null)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(s.id) as sent_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'dynamic_content_stats', 's');
|
||||
|
||||
if ($dynamicContentIds) {
|
||||
if (!is_array($dynamicContentIds)) {
|
||||
$dynamicContentIds = [(int) $dynamicContentIds];
|
||||
}
|
||||
$q->where(
|
||||
$q->expr()->in('s.dynamic_content_id', $dynamicContentIds)
|
||||
);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return (isset($results[0])) ? $results[0]['sent_count'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sent counts based grouped by dynamic content Id.
|
||||
*
|
||||
* @param array $dynamicContentIds
|
||||
*/
|
||||
public function getSentCounts($dynamicContentIds = [], ?\DateTime $fromDate = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('s.dynamic_content_id, count(s.id) as sent_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'dynamic_content_stats', 's')
|
||||
->andWhere(
|
||||
$q->expr()->in('e.dynamic_content_id', $dynamicContentIds)
|
||||
);
|
||||
|
||||
if (null !== $fromDate) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($fromDate);
|
||||
$q->andWhere(
|
||||
$q->expr()->gte('e.date_sent', $q->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
$q->groupBy('e.dynamic_content_id');
|
||||
|
||||
// get a total number of sent DC stats first
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$counts = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$counts[$r['dynamic_content_id']] = $r['sent_count'];
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's dynamic content stat.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getLeadStats($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select('dc.id AS dynamic_content_id, s.id, s.date_sent as dateSent, dc.name, s.sent_details as sentDetails, s.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'dynamic_content_stats', 's')
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'dynamic_content', 'dc', 'dc.id = s.dynamic_content_id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('s.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere('dc.name LIKE :search')
|
||||
->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'dc.name', 's.date_sent', ['sentDetails'], ['dateSent'], null, 's.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'dynamic_content_stats')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a stat.
|
||||
*/
|
||||
public function deleteStat($id): void
|
||||
{
|
||||
$this->_em->getConnection()->delete(MAUTIC_TABLE_PREFIX.'dynamic_content_stats', ['id' => (int) $id]);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 's';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class ContactFiltersEvaluateEvent extends Event
|
||||
{
|
||||
private bool $isEvaluated = false;
|
||||
|
||||
private bool $isMatched = false;
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*/
|
||||
public function __construct(
|
||||
private array $filters,
|
||||
private Lead $contact,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isMatch(): bool
|
||||
{
|
||||
return $this->isEvaluated() && $this->isMatched;
|
||||
}
|
||||
|
||||
public function isEvaluated(): bool
|
||||
{
|
||||
return $this->isEvaluated;
|
||||
}
|
||||
|
||||
public function setIsEvaluated(bool $evaluated): ContactFiltersEvaluateEvent
|
||||
{
|
||||
$this->isEvaluated = $evaluated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContact(): Lead
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
public function setContact(Lead $contact): ContactFiltersEvaluateEvent
|
||||
{
|
||||
$this->contact = $contact;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFilters(): array
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function isMatched(): bool
|
||||
{
|
||||
return $this->isMatched;
|
||||
}
|
||||
|
||||
public function setIsMatched(bool $isMatched): ContactFiltersEvaluateEvent
|
||||
{
|
||||
$this->isMatched = $isMatched;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
|
||||
class DynamicContentEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(DynamicContent $entity, $isNew = false)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DynamicContent
|
||||
*/
|
||||
public function getDynamicContent()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function setDynamicContent(DynamicContent $entity): void
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\BuildJsEvent;
|
||||
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class BuildJsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AssetsHelper $assetsHelper,
|
||||
private TranslatorInterface $translator,
|
||||
private RequestStack $requestStack,
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::BUILD_MAUTIC_JS => ['onBuildJs', 200],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the MauticJS definition and core
|
||||
* JS functions for use in Bundles. This
|
||||
* must retain top priority of 1000.
|
||||
*/
|
||||
public function onBuildJs(BuildJsEvent $event): void
|
||||
{
|
||||
$dwcUrl = $this->router->generate('mautic_api_dynamicContent_action', ['objectAlias' => 'slotNamePlaceholder'], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$js = <<<JS
|
||||
|
||||
// call variable if doesnt exist
|
||||
if (typeof MauticDomain == 'undefined') {
|
||||
var MauticDomain = '{$this->requestStack->getCurrentRequest()->getSchemeAndHttpHost()}';
|
||||
}
|
||||
if (typeof MauticLang == 'undefined') {
|
||||
var MauticLang = {
|
||||
'submittingMessage': "{$this->translator->trans('mautic.form.submission.pleasewait')}"
|
||||
};
|
||||
}
|
||||
MauticJS.replaceDynamicContent = function (params) {
|
||||
params = params || {};
|
||||
|
||||
var dynamicContentSlots = document.querySelectorAll('.mautic-slot, [data-slot="dwc"]');
|
||||
if (dynamicContentSlots.length) {
|
||||
MauticJS.iterateCollection(dynamicContentSlots)(function(node, i) {
|
||||
var slotName = node.dataset['slotName'];
|
||||
if ('undefined' === typeof slotName) {
|
||||
slotName = node.dataset['paramSlotName'];
|
||||
}
|
||||
if ('undefined' === typeof slotName) {
|
||||
node.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
var url = '{$dwcUrl}'.replace('slotNamePlaceholder', slotName);
|
||||
|
||||
MauticJS.makeCORSRequest('GET', url, params, function(response, xhr) {
|
||||
if (response.content) {
|
||||
var dwcContent = response.content;
|
||||
node.innerHTML = dwcContent;
|
||||
|
||||
if (response.id && response.sid) {
|
||||
MauticJS.setTrackedContact(response);
|
||||
}
|
||||
|
||||
// form load library
|
||||
if (dwcContent.search("mauticform_wrapper") > 0) {
|
||||
// if doesn't exist
|
||||
if (typeof MauticSDK == 'undefined') {
|
||||
MauticJS.insertScript('{$this->assetsHelper->getUrl('media/js/mautic-form.js', null, null, true)}');
|
||||
|
||||
// check initialize form library
|
||||
var fileInterval = setInterval(function() {
|
||||
if (typeof MauticSDK != 'undefined') {
|
||||
MauticSDK.onLoad();
|
||||
clearInterval(fileInterval); // clear interval
|
||||
}
|
||||
}, 100); // check every 100ms
|
||||
} else {
|
||||
MauticSDK.onLoad();
|
||||
}
|
||||
}
|
||||
|
||||
var m;
|
||||
var regEx = /<script[^>]+src="?([^"\s]+)"?\s/g;
|
||||
|
||||
while (m = regEx.exec(dwcContent)) {
|
||||
if ((m[1]).search("/focus/") > 0) {
|
||||
MauticJS.insertScript(m[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MauticJS.beforeFirstEventDelivery(MauticJS.replaceDynamicContent);
|
||||
JS;
|
||||
$event->appendJs($js, 'Mautic Dynamic Content');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Mautic\CacheBundle\Cache\CacheProvider;
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\DynamicContentBundle\DynamicContentEvents;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Form\Type\DynamicContentDecisionType;
|
||||
use Mautic\DynamicContentBundle\Form\Type\DynamicContentSendType;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class CampaignSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DynamicContentModel $dynamicContentModel,
|
||||
protected CacheProvider $cache,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
|
||||
DynamicContentEvents::ON_CAMPAIGN_TRIGGER_DECISION => ['onCampaignTriggerDecision', 0],
|
||||
DynamicContentEvents::ON_CAMPAIGN_TRIGGER_ACTION => ['onCampaignTriggerAction', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onCampaignBuild(CampaignBuilderEvent $event): void
|
||||
{
|
||||
$event->addAction(
|
||||
'dwc.push_content',
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.campaign.send_dwc',
|
||||
'description' => 'mautic.dynamicContent.campaign.send_dwc.tooltip',
|
||||
'eventName' => DynamicContentEvents::ON_CAMPAIGN_TRIGGER_ACTION,
|
||||
'formType' => DynamicContentSendType::class,
|
||||
'formTypeOptions' => ['update_select' => 'campaignevent_properties_dynamicContent'],
|
||||
'formTheme' => '@MauticDynamicContent/FormTheme/DynamicContentPushList/_dynamiccontentpush_list_row.html.twig',
|
||||
'timelineTemplate' => '@MauticDynamicContent/SubscribedEvents/Timeline/index.html.twig',
|
||||
'hideTriggerMode' => true,
|
||||
'connectionRestrictions' => [
|
||||
'anchor' => [
|
||||
'decision.inaction',
|
||||
],
|
||||
'source' => [
|
||||
'decision' => [
|
||||
'dwc.decision',
|
||||
],
|
||||
],
|
||||
],
|
||||
'channel' => 'dynamicContent',
|
||||
'channelIdField' => 'dwc_slot_name',
|
||||
]
|
||||
);
|
||||
|
||||
$event->addDecision(
|
||||
'dwc.decision',
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.campaign.decision_dwc',
|
||||
'description' => 'mautic.dynamicContent.campaign.decision_dwc.tooltip',
|
||||
'eventName' => DynamicContentEvents::ON_CAMPAIGN_TRIGGER_DECISION,
|
||||
'formType' => DynamicContentDecisionType::class,
|
||||
'formTypeOptions' => ['update_select' => 'campaignevent_properties_dynamicContent'],
|
||||
'formTheme' => '@MauticDynamicContent/FormTheme/DynamicContentDecisionList/_dynamiccontentdecision_list_row.html.twig',
|
||||
'channel' => 'dynamicContent',
|
||||
'channelIdField' => 'dynamicContent',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|CampaignExecutionEvent
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function onCampaignTriggerDecision(CampaignExecutionEvent $event)
|
||||
{
|
||||
$eventConfig = $event->getConfig();
|
||||
$eventDetails = $event->getEventDetails();
|
||||
$lead = $event->getLead();
|
||||
|
||||
// stop
|
||||
if ($eventConfig['dwc_slot_name'] !== $eventDetails) {
|
||||
$event->setResult(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$defaultDwc = $this->dynamicContentModel->getRepository()->getEntity($eventConfig['dynamicContent']);
|
||||
|
||||
if ($defaultDwc instanceof DynamicContent) {
|
||||
// Set the default content in case none of the actions return data
|
||||
$this->dynamicContentModel->setSlotContentForLead($defaultDwc, $lead, $eventDetails);
|
||||
}
|
||||
|
||||
$item = $this->cache->getItem('dwc.slot_name.lead.'.$lead->getId());
|
||||
$item->set($eventDetails);
|
||||
$item->expiresAfter(86400); // one day in seconds
|
||||
$this->cache->save($item);
|
||||
|
||||
$event->stopPropagation();
|
||||
|
||||
return $event->setResult(true);
|
||||
}
|
||||
|
||||
public function onCampaignTriggerAction(CampaignExecutionEvent $event)
|
||||
{
|
||||
$eventConfig = $event->getConfig();
|
||||
$lead = $event->getLead();
|
||||
/* @var CacheItemInterface $item */
|
||||
$item = $this->cache->getItem('dwc.slot_name.lead.'.$lead->getId());
|
||||
$slot = $item->get();
|
||||
|
||||
$dwc = $this->dynamicContentModel->getRepository()->getEntity($eventConfig['dynamicContent']);
|
||||
|
||||
if ($dwc instanceof DynamicContent) {
|
||||
// Use translation if available
|
||||
list($ignore, $dwc) = $this->dynamicContentModel->getTranslatedEntity($dwc, $lead);
|
||||
\assert($dwc instanceof DynamicContent);
|
||||
|
||||
if ($slot) {
|
||||
$this->dynamicContentModel->setSlotContentForLead($dwc, $lead, $slot);
|
||||
}
|
||||
|
||||
$stat = $this->dynamicContentModel->createStatEntry($dwc, $lead, $slot);
|
||||
|
||||
$tokenEvent = new TokenReplacementEvent($dwc->getContent(), $lead, ['slot' => $slot, 'dynamic_content_id' => $dwc->getId()]);
|
||||
$tokenEvent->setStat($stat);
|
||||
$this->dispatcher->dispatch($tokenEvent, DynamicContentEvents::TOKEN_REPLACEMENT);
|
||||
|
||||
$content = $tokenEvent->getContent();
|
||||
$content = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content);
|
||||
|
||||
$event->stopPropagation();
|
||||
|
||||
$result = $event->setResult($content);
|
||||
$event->setChannel('dynamicContent', $dwc->getId());
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Event\ChannelEvent;
|
||||
use Mautic\ReportBundle\Model\ReportModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ChannelSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ChannelEvents::ADD_CHANNEL => ['onAddChannel', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onAddChannel(ChannelEvent $event): void
|
||||
{
|
||||
$event->addChannel(
|
||||
'dynamicContent',
|
||||
[
|
||||
ReportModel::CHANNEL_FEATURE => [
|
||||
'table' => 'dynamic_content',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Event\EntityExportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportAnalyzeEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportUndoEvent;
|
||||
use Mautic\CoreBundle\EventListener\ImportExportTrait;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
final class DynamicContentImportExportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use ImportExportTrait;
|
||||
|
||||
public function __construct(
|
||||
private DynamicContentModel $dynamicContentModel,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AuditLogModel $auditLogModel,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private DenormalizerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EntityExportEvent::class => ['onExport', 0],
|
||||
EntityImportEvent::class => ['onImport', 0],
|
||||
EntityImportUndoEvent::class => ['onUndoImport', 0],
|
||||
EntityImportAnalyzeEvent::class => ['onDuplicationCheck', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onExport(EntityExportEvent $event): void
|
||||
{
|
||||
if (DynamicContent::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$object = $this->dynamicContentModel->getEntity($event->getEntityId());
|
||||
if (!$object) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id' => $object->getId(),
|
||||
'translation_parent_id' => $object->getTranslationParent(),
|
||||
'variant_parent_id' => $object->getVariantParent(),
|
||||
'is_published' => $object->getIsPublished(),
|
||||
'name' => $object->getName(),
|
||||
'description' => $object->getDescription(),
|
||||
'publish_up' => $object->getPublishUp(),
|
||||
'publish_down' => $object->getPublishDown(),
|
||||
'content' => $object->getContent(),
|
||||
'utm_tags' => $object->getUtmTags(),
|
||||
'lang' => $object->getLanguage(),
|
||||
'variant_settings' => $object->getVariantSettings(),
|
||||
'variant_start_date' => $object->getVariantStartDate(),
|
||||
'filters' => $object->getFilters(),
|
||||
'is_campaign_based' => $object->getIsCampaignBased(),
|
||||
'slot_name' => $object->getSlotName(),
|
||||
'uuid' => $object->getUuid(),
|
||||
];
|
||||
$event->addEntity(DynamicContent::ENTITY_NAME, $data);
|
||||
|
||||
$this->logAction('export', $object->getId(), $data);
|
||||
}
|
||||
|
||||
public function onImport(EntityImportEvent $event): void
|
||||
{
|
||||
if (DynamicContent::ENTITY_NAME !== $event->getEntityName() || !$event->getEntityData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
EntityImportEvent::NEW => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
EntityImportEvent::UPDATE => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
];
|
||||
|
||||
foreach ($event->getEntityData() as $element) {
|
||||
$object = $this->entityManager->getRepository(DynamicContent::class)->findOneBy(['uuid' => $element['uuid']]);
|
||||
$isNew = !$object;
|
||||
|
||||
$object ??= new DynamicContent();
|
||||
$this->serializer->denormalize(
|
||||
$element,
|
||||
DynamicContent::class,
|
||||
null,
|
||||
['object_to_populate' => $object]
|
||||
);
|
||||
|
||||
$this->dynamicContentModel->saveEntity($object);
|
||||
|
||||
$event->addEntityIdMap((int) $element['id'], $object->getId());
|
||||
|
||||
$status = $isNew ? EntityImportEvent::NEW : EntityImportEvent::UPDATE;
|
||||
$stats[$status]['names'][] = $object->getName();
|
||||
$stats[$status]['ids'][] = $object->getId();
|
||||
++$stats[$status]['count'];
|
||||
|
||||
$this->logAction('import', $object->getId(), $element);
|
||||
}
|
||||
|
||||
foreach ($stats as $status => $info) {
|
||||
if ($info['count'] > 0) {
|
||||
$event->setStatus($status, [DynamicContent::ENTITY_NAME => $info]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUndoImport(EntityImportUndoEvent $event): void
|
||||
{
|
||||
if (DynamicContent::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = $event->getSummary();
|
||||
|
||||
if (!isset($summary['ids']) || empty($summary['ids'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($summary['ids'] as $id) {
|
||||
$entity = $this->entityManager->getRepository(DynamicContent::class)->find($id);
|
||||
|
||||
if ($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
$this->logAction('undo_import', $id, ['deletedEntity' => DynamicContent::class]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function onDuplicationCheck(EntityImportAnalyzeEvent $event): void
|
||||
{
|
||||
$this->performDuplicationCheck(
|
||||
$event,
|
||||
DynamicContent::ENTITY_NAME,
|
||||
DynamicContent::class,
|
||||
'name',
|
||||
$this->entityManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
private function logAction(string $action, int $objectId, array $details): void
|
||||
{
|
||||
$this->auditLogModel->writeToLog([
|
||||
'bundle' => 'dynamicContent',
|
||||
'object' => 'dynamicContent',
|
||||
'objectId' => $objectId,
|
||||
'action' => $action,
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Helper\TokenHelper as AssetTokenHelper;
|
||||
use Mautic\CoreBundle\Event as MauticEvents;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DynamicContentBundle\DynamicContentEvents;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Event as Events;
|
||||
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Mautic\EmailBundle\EventListener\MatchFilterForLeadTrait;
|
||||
use Mautic\FormBundle\Helper\TokenHelper as FormTokenHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Exception\PrimaryCompanyNotFoundException;
|
||||
use Mautic\LeadBundle\Helper\TokenHelper;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use Mautic\PageBundle\Event\PageDisplayEvent;
|
||||
use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use MauticPlugin\MauticFocusBundle\Helper\TokenHelper as FocusTokenHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class DynamicContentSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use MatchFilterForLeadTrait;
|
||||
|
||||
public function __construct(
|
||||
private TrackableModel $trackableModel,
|
||||
private PageTokenHelper $pageTokenHelper,
|
||||
private AssetTokenHelper $assetTokenHelper,
|
||||
private FormTokenHelper $formTokenHelper,
|
||||
private FocusTokenHelper $focusTokenHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
private DynamicContentHelper $dynamicContentHelper,
|
||||
private DynamicContentModel $dynamicContentModel,
|
||||
private CorePermissions $security,
|
||||
private ContactTracker $contactTracker,
|
||||
private CompanyModel $companyModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
DynamicContentEvents::POST_SAVE => ['onPostSave', 0],
|
||||
DynamicContentEvents::POST_DELETE => ['onDelete', 0],
|
||||
DynamicContentEvents::TOKEN_REPLACEMENT => ['onTokenReplacement', 0],
|
||||
PageEvents::PAGE_ON_DISPLAY => ['decodeTokens', 254],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onPostSave(Events\DynamicContentEvent $event): void
|
||||
{
|
||||
$entity = $event->getDynamicContent();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'dynamicContent',
|
||||
'object' => 'dynamicContent',
|
||||
'objectId' => $entity->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onDelete(Events\DynamicContentEvent $event): void
|
||||
{
|
||||
$entity = $event->getDynamicContent();
|
||||
$log = [
|
||||
'bundle' => 'dynamicContent',
|
||||
'object' => 'dynamicContent',
|
||||
'objectId' => $entity->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $entity->getName()],
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
public function onTokenReplacement(MauticEvents\TokenReplacementEvent $event): void
|
||||
{
|
||||
/** @var Lead $lead */
|
||||
$lead = $event->getLead();
|
||||
$content = $event->getContent();
|
||||
$clickthrough = $event->getClickthrough();
|
||||
|
||||
if ($lead instanceof Lead && $content) {
|
||||
$leadArray = $lead->getProfileFields();
|
||||
try {
|
||||
$primaryCompany = $this->companyModel->getCompanyLeadRepository()->getPrimaryCompanyByLeadId($lead->getId());
|
||||
$leadArray['companies'] = [$primaryCompany];
|
||||
} catch (PrimaryCompanyNotFoundException) {
|
||||
}
|
||||
$tokens = array_merge(
|
||||
TokenHelper::findLeadTokens($content, $leadArray),
|
||||
$this->pageTokenHelper->findPageTokens($content, $clickthrough),
|
||||
$this->assetTokenHelper->findAssetTokens($content, $clickthrough),
|
||||
$this->formTokenHelper->findFormTokens($content),
|
||||
$this->focusTokenHelper->findFocusTokens($content)
|
||||
);
|
||||
|
||||
[$content, $trackables] = $this->trackableModel->parseContentForTrackables(
|
||||
$content,
|
||||
$tokens,
|
||||
'dynamicContent',
|
||||
$clickthrough['dynamic_content_id']
|
||||
);
|
||||
|
||||
$dwc = $this->dynamicContentModel->getEntity($clickthrough['dynamic_content_id']);
|
||||
$utmTags = [];
|
||||
if ($dwc && $dwc instanceof DynamicContent) {
|
||||
$utmTags = $dwc->getUtmTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string $token
|
||||
* @var Trackable $trackable
|
||||
*/
|
||||
foreach ($trackables as $token => $trackable) {
|
||||
$tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, $clickthrough, false, $utmTags);
|
||||
}
|
||||
|
||||
$content = str_replace(array_keys($tokens), array_values($tokens), $content);
|
||||
|
||||
$event->setContent($content);
|
||||
}
|
||||
}
|
||||
|
||||
public function decodeTokens(PageDisplayEvent $event): void
|
||||
{
|
||||
if (!$lead = $event->getLead()) {
|
||||
$lead = $this->security->isAnonymous() ? $this->contactTracker->getContact() : null;
|
||||
}
|
||||
|
||||
if (!$lead) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $event->getContent();
|
||||
if (empty($content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tokens = $this->dynamicContentHelper->findDwcTokens($content, $lead);
|
||||
|
||||
// replace slots
|
||||
$dom = new \DOMDocument('1.0', 'utf-8');
|
||||
$dom->loadHTML(mb_encode_numericentity($content, [0x80, 0x10FFFF, 0, 0xFFFFF], 'UTF-8'), LIBXML_NOERROR);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
$contentSlots = $xpath->query('//*[@data-slot="dwc"]');
|
||||
|
||||
for ($i = 0; $i < $contentSlots->length; ++$i) {
|
||||
$slot = $contentSlots->item($i);
|
||||
if (!$slotName = $slot->getAttribute('data-param-slot-name')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$slotContent = $this->dynamicContentHelper->getDynamicContentForLead($slotName, $lead)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newnode = $dom->createDocumentFragment();
|
||||
$newnode->appendXML('<![CDATA['.mb_encode_numericentity($slotContent, [0x80, 0x10FFFF, 0, 0xFFFFF], 'UTF-8').']]>');
|
||||
$slot->parentNode->replaceChild($newnode, $slot);
|
||||
}
|
||||
|
||||
$content = $dom->saveHTML();
|
||||
|
||||
// These tokens need to be replaced after the content, because otherwise the replaced tokens will have encoded
|
||||
// HTML entities, which do not conform the tests.
|
||||
$result = [];
|
||||
foreach ($tokens as $token => $dwc) {
|
||||
$result[$token] = $dwc['content'];
|
||||
}
|
||||
$content = str_replace(array_keys($result), array_values($result), $content);
|
||||
|
||||
$event->setContent($content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Mautic\DynamicContentBundle\Entity\StatRepository;
|
||||
use Mautic\LeadBundle\Event\LeadMergeEvent;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private RouterInterface $router,
|
||||
private StatRepository $statRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::TIMELINE_ON_GENERATE => ['onTimelineGenerate', 0],
|
||||
LeadEvents::LEAD_POST_MERGE => ['onLeadMerge', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
// Set available event types
|
||||
$eventTypeKey = 'dynamic.content.sent';
|
||||
$eventTypeNameSent = $this->translator->trans('mautic.dynamic.content.triggered');
|
||||
$event->addEventType($eventTypeKey, $eventTypeNameSent);
|
||||
$event->addSerializerGroup('dwcList');
|
||||
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $this->statRepository->getLeadStats($event->getLeadId(), $event->getQueryOptions());
|
||||
|
||||
// Add total number to counter
|
||||
$event->addToCounter($eventTypeKey, $stats);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the events to the event array
|
||||
foreach ($stats['results'] as $stat) {
|
||||
$contactId = $stat['lead_id'];
|
||||
unset($stat['lead_id']);
|
||||
if ($stat['dateSent']) {
|
||||
$event->addEvent(
|
||||
[
|
||||
'event' => $eventTypeKey,
|
||||
'eventId' => $eventTypeKey.$stat['id'],
|
||||
'eventLabel' => [
|
||||
'label' => $stat['name'],
|
||||
'href' => $this->router->generate(
|
||||
'mautic_dynamicContent_action',
|
||||
['objectId' => $stat['dynamic_content_id'], 'objectAction' => 'view']
|
||||
),
|
||||
],
|
||||
'eventType' => $eventTypeNameSent,
|
||||
'timestamp' => $stat['dateSent'],
|
||||
'extra' => [
|
||||
'stat' => $stat,
|
||||
'type' => 'sent',
|
||||
],
|
||||
'contentTemplate' => '@MauticDynamicContent/SubscribedEvents/Timeline/index.html.twig',
|
||||
'icon' => 'ri-puzzle-2-line',
|
||||
'contactId' => $contactId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onLeadMerge(LeadMergeEvent $event): void
|
||||
{
|
||||
$this->statRepository->updateLead(
|
||||
$event->getLoser()->getId(),
|
||||
$event->getVictor()->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event\GlobalSearchEvent;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DynamicContentModel $dynamicContentModel,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => ['onGlobalSearch', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearch(GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->dynamicContentModel,
|
||||
'@MauticDynamicContent/SubscribedEvents/Search/global.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.dynamicContent.dynamicContent', $results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContentLeadData;
|
||||
use Mautic\DynamicContentBundle\Entity\Stat;
|
||||
|
||||
class StatsSubscriber extends CommonStatsSubscriber
|
||||
{
|
||||
public function __construct(CorePermissions $security, EntityManager $entityManager)
|
||||
{
|
||||
parent::__construct($security, $entityManager);
|
||||
$this->addContactRestrictedRepositories(
|
||||
[
|
||||
Stat::class,
|
||||
DynamicContentLeadData::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Form\Type;
|
||||
|
||||
use Mautic\LeadBundle\Form\Type\FilterTrait;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\Exception\AccessException;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class DwcEntryFiltersType extends AbstractType
|
||||
{
|
||||
use FilterTrait;
|
||||
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private ListModel $listModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'glue',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'label' => false,
|
||||
'choices' => [
|
||||
'mautic.lead.list.form.glue.and' => 'and',
|
||||
'mautic.lead.list.form.glue.or' => 'or',
|
||||
],
|
||||
'attr' => [
|
||||
'class' => 'form-control not-chosen glue-select',
|
||||
'onchange' => 'Mautic.updateFilterPositioning(this)',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$formModifier = function (FormEvent $event, $eventName): void {
|
||||
$this->buildFiltersForm($eventName, $event, $this->translator);
|
||||
};
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) use ($formModifier): void {
|
||||
$formModifier($event, FormEvents::PRE_SET_DATA);
|
||||
}
|
||||
);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($formModifier): void {
|
||||
$formModifier($event, FormEvents::PRE_SUBMIT);
|
||||
}
|
||||
);
|
||||
|
||||
$builder->add('field', HiddenType::class);
|
||||
$builder->add('object', HiddenType::class);
|
||||
$builder->add('type', HiddenType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AccessException
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired(
|
||||
[
|
||||
'countries',
|
||||
'regions',
|
||||
'timezones',
|
||||
'locales',
|
||||
'fields',
|
||||
'deviceTypes',
|
||||
'deviceBrands',
|
||||
'deviceOs',
|
||||
'tags',
|
||||
'lists',
|
||||
]
|
||||
);
|
||||
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'label' => false,
|
||||
'error_bubbling' => false,
|
||||
// @see \Mautic\LeadBundle\Controller\AjaxController::loadSegmentFilterFormAction()
|
||||
'lists' => $this->listModel->getChoiceFields()['lead']['leadlist']['properties']['list'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['fields'] = $options['fields'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class DynamicContentDecisionType extends DynamicContentSendType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'dwc_slot_name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.send.slot_name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.dynamicContent.send.slot_name.tooltip',
|
||||
],
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(['message' => 'mautic.core.value.required']),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
parent::buildForm($builder, $options);
|
||||
|
||||
$builder->add(
|
||||
'dynamicContent',
|
||||
DynamicContentListType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.send.selectDynamicContents.default',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.dynamicContent.choose.dynamicContents',
|
||||
'onchange' => 'Mautic.disabledDynamicContentAction()',
|
||||
],
|
||||
'where' => 'e.isCampaignBased = 1', // do not show dwc with filters
|
||||
'multiple' => false,
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(['message' => 'mautic.core.value.required']),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'dwcdecision_list';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\EntityLookupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class DynamicContentListType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'modal_route' => 'mautic_dynamicContent_action',
|
||||
'modal_header' => 'mautic.dynamicContent.header.new',
|
||||
'model' => 'dynamicContent',
|
||||
'model_lookup_method' => 'getLookupResults',
|
||||
'lookup_arguments' => fn (Options $options): array => [
|
||||
'type' => 'dynamicContent',
|
||||
'filter' => '$data',
|
||||
'limit' => 0,
|
||||
'start' => 0,
|
||||
'options' => [
|
||||
'top_level' => $options['top_level'],
|
||||
'ignore_ids' => $options['ignore_ids'],
|
||||
'where' => $options['where'],
|
||||
],
|
||||
],
|
||||
'ajax_lookup_action' => function (Options $options): string {
|
||||
$query = [
|
||||
'top_level' => $options['top_level'],
|
||||
'ignore_ids' => $options['ignore_ids'],
|
||||
'where' => $options['where'],
|
||||
];
|
||||
|
||||
return 'dynamicContent:getLookupChoiceList&'.http_build_query($query);
|
||||
},
|
||||
'multiple' => false,
|
||||
'required' => false,
|
||||
'top_level' => 'translation',
|
||||
'ignore_ids' => [],
|
||||
'where' => '',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'dwc_list';
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return EntityLookupType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class DynamicContentSendType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
protected RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'dynamicContent',
|
||||
DynamicContentListType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.send.selectDynamicContents',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.dynamicContent.choose.dynamicContents',
|
||||
'onchange' => 'Mautic.disabledDynamicContentAction()',
|
||||
],
|
||||
'where' => 'e.isCampaignBased = 1', // do not show dwc with filters
|
||||
'multiple' => false,
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(['message' => 'mautic.core.value.required']),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['update_select'])) {
|
||||
$windowUrl = $this->router->generate(
|
||||
'mautic_dynamicContent_action',
|
||||
[
|
||||
'objectAction' => 'new',
|
||||
'contentOnly' => 1,
|
||||
'updateSelect' => $options['update_select'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'newDynamicContentButton',
|
||||
ButtonType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.send.new.dynamicContent',
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-nospin',
|
||||
'onclick' => 'Mautic.loadNewWindow({
|
||||
"windowUrl": "'.$windowUrl.'"
|
||||
})',
|
||||
'icon' => 'ri-add-line',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$dynamicContent = is_array($options['data']) && array_key_exists('dynamicContent', $options['data']) ? $options['data']['dynamicContent']
|
||||
: null;
|
||||
|
||||
// create button edit notification
|
||||
$windowUrlEdit = $this->router->generate(
|
||||
'mautic_dynamicContent_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => 'dynamicContentId',
|
||||
'contentOnly' => 1,
|
||||
'updateSelect' => $options['update_select'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'editDynamicContentButton',
|
||||
ButtonType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.send.edit.dynamicContent',
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-nospin',
|
||||
'onclick' => 'Mautic.loadNewWindow(Mautic.standardDynamicContentUrl({"windowUrl": "'.$windowUrlEdit.'"}))',
|
||||
'disabled' => !isset($dynamicContent),
|
||||
'icon' => 'ri-edit-line',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined(['update_select']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'dwcsend_list';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Form\Type;
|
||||
|
||||
use DeviceDetector\Parser\Device\AbstractDeviceParser as DeviceParser;
|
||||
use DeviceDetector\Parser\OperatingSystem;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\DynamicContentBundle\DynamicContent\TypeList;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\EmailBundle\Form\Type\EmailUtmTagsType;
|
||||
use Mautic\LeadBundle\Form\DataTransformer\FieldFilterTransformer;
|
||||
use Mautic\LeadBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<DynamicContent>
|
||||
*/
|
||||
class DynamicContentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $fieldChoices;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $countryChoices;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $regionChoices;
|
||||
|
||||
private $timezoneChoices;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $localeChoices;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $deviceTypesChoices;
|
||||
|
||||
private $deviceBrandsChoices;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $deviceOsChoices;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $tagChoices = [];
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
private EntityManager $em,
|
||||
ListModel $listModel,
|
||||
private TranslatorInterface $translator,
|
||||
private LeadModel $leadModel,
|
||||
private TypeList $typeList,
|
||||
) {
|
||||
$this->fieldChoices = $listModel->getChoiceFields();
|
||||
$this->timezoneChoices = FormFieldHelper::getTimezonesChoices();
|
||||
$this->countryChoices = FormFieldHelper::getCountryChoices();
|
||||
$this->regionChoices = FormFieldHelper::getRegionChoices();
|
||||
$this->localeChoices = FormFieldHelper::getLocaleChoices();
|
||||
$this->typeList = $typeList;
|
||||
|
||||
$this->filterFieldChoices();
|
||||
|
||||
$tags = $leadModel->getTagList();
|
||||
foreach ($tags as $tag) {
|
||||
$this->tagChoices[$tag['value']] = $tag['label'];
|
||||
}
|
||||
|
||||
$this->deviceTypesChoices = array_combine(DeviceParser::getAvailableDeviceTypeNames(), DeviceParser::getAvailableDeviceTypeNames());
|
||||
$this->deviceBrandsChoices = DeviceParser::$deviceBrands;
|
||||
$this->deviceOsChoices = array_combine(
|
||||
array_keys(OperatingSystem::getAvailableOperatingSystemFamilies()),
|
||||
array_keys(OperatingSystem::getAvailableOperatingSystemFamilies())
|
||||
);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('dynamicContent.dynamicContent', $options));
|
||||
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.form.internal.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'slotName',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.send.slot_name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.dynamicContent.send.slot_name.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.dynamicContent.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('type', ChoiceType::class, [
|
||||
'label' => 'mautic.dynamicContent.type.label',
|
||||
'choices' => $this->typeList->getChoices(),
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'onchange' => 'Mautic.toggleContentEditor()',
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add('isPublished', YesNoButtonGroupType::class, [
|
||||
'label' => 'mautic.core.form.available',
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
'isCampaignBased',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.dwc.form.is_campaign_based',
|
||||
'data' => (bool) $options['data']->isCampaignBased(),
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.dwc.form.is_campaign_based.tooltip',
|
||||
'onchange' => 'Mautic.toggleDwcFilters()',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'language',
|
||||
LocaleType::class,
|
||||
[
|
||||
'label' => 'mautic.core.language',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('publishUp', PublishUpDateType::class);
|
||||
$builder->add('publishDown', PublishDownDateType::class);
|
||||
|
||||
$builder->add(
|
||||
'utmTags',
|
||||
EmailUtmTagsType::class,
|
||||
[
|
||||
'label' => 'mautic.email.utm_tags',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.email.utm_tags.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$transformer = new IdToEntityModelTransformer($this->em, DynamicContent::class);
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'translationParent',
|
||||
DynamicContentListType::class,
|
||||
[
|
||||
'label' => 'mautic.core.form.translation_parent',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.core.form.translation_parent.help',
|
||||
],
|
||||
'required' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'mautic.core.form.translation_parent.empty',
|
||||
'top_level' => 'translation',
|
||||
'ignore_ids' => [(int) $options['data']->getId()],
|
||||
]
|
||||
)->addModelTransformer($transformer)
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
['bundle' => 'dynamicContent']
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
|
||||
if (!empty($options['update_select'])) {
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
['apply_text' => false]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'updateSelect',
|
||||
HiddenType::class,
|
||||
[
|
||||
'data' => $options['update_select'],
|
||||
'mapped' => false,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class
|
||||
);
|
||||
}
|
||||
|
||||
$filterModalTransformer = new FieldFilterTransformer($this->translator);
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'filters',
|
||||
CollectionType::class,
|
||||
[
|
||||
'entry_type' => DwcEntryFiltersType::class,
|
||||
'entry_options' => [
|
||||
'countries' => $this->countryChoices,
|
||||
'regions' => $this->regionChoices,
|
||||
'timezones' => $this->timezoneChoices,
|
||||
'locales' => $this->localeChoices,
|
||||
'fields' => $this->fieldChoices,
|
||||
'deviceTypes' => $this->deviceTypesChoices,
|
||||
'deviceBrands' => $this->deviceBrandsChoices,
|
||||
'deviceOs' => $this->deviceOsChoices,
|
||||
'tags' => $this->tagChoices,
|
||||
],
|
||||
'error_bubbling' => false,
|
||||
'mapped' => true,
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
]
|
||||
)->addModelTransformer($filterModalTransformer)
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event): void {
|
||||
// delete default prototype values
|
||||
$data = $event->getData();
|
||||
unset($data['filters']['__name__']);
|
||||
$event->setData($data);
|
||||
}
|
||||
);
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
||||
/** @var DynamicContent|null $dynamicContent */
|
||||
$dynamicContent = $event->getData();
|
||||
$this->addContentField($event->getForm(), $dynamicContent?->getType());
|
||||
});
|
||||
|
||||
$builder->get('type')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
|
||||
$form = $event->getForm();
|
||||
$this->addContentField($form->getParent(), $form->getData());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Component\OptionsResolver\Exception\AccessException
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => DynamicContent::class,
|
||||
'label' => false,
|
||||
'error_bubbling' => false,
|
||||
]);
|
||||
|
||||
$resolver->setDefined(['update_select']);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['fields'] = $this->fieldChoices;
|
||||
$view->vars['countries'] = $this->countryChoices;
|
||||
$view->vars['regions'] = $this->regionChoices;
|
||||
$view->vars['timezones'] = $this->timezoneChoices;
|
||||
$view->vars['deviceTypes'] = $this->deviceTypesChoices;
|
||||
$view->vars['deviceBrands'] = $this->deviceBrandsChoices;
|
||||
$view->vars['deviceOs'] = $this->deviceOsChoices;
|
||||
$view->vars['tags'] = $this->tagChoices;
|
||||
$view->vars['locales'] = $this->localeChoices;
|
||||
}
|
||||
|
||||
private function filterFieldChoices(): void
|
||||
{
|
||||
unset($this->fieldChoices['company']);
|
||||
|
||||
$customFields = $this->leadModel->getRepository()->getCustomFieldList('lead');
|
||||
|
||||
$this->fieldChoices['lead'] = array_filter(
|
||||
$this->fieldChoices['lead'],
|
||||
fn ($key): bool => in_array(
|
||||
$key,
|
||||
array_merge(
|
||||
array_keys($customFields[0]),
|
||||
['date_added', 'date_modified', 'device_brand', 'device_model', 'device_os', 'device_type', 'tags', 'leadlist']
|
||||
),
|
||||
true
|
||||
),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface<FormInterface> $form
|
||||
*/
|
||||
private function addContentField(FormInterface $form, ?string $type): void
|
||||
{
|
||||
$enableEditor = TypeList::HTML === $type;
|
||||
$editorClass = 'editor editor-advanced editor-builder-tokens';
|
||||
|
||||
$form->add('content', TextareaType::class, [
|
||||
'label' => 'mautic.dynamicContent.form.content',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.dynamicContent.form.content.help',
|
||||
'class' => 'form-control'.($enableEditor ? ' '.$editorClass : ''),
|
||||
'rows' => 15,
|
||||
'data-editor-enable' => $enableEditor,
|
||||
'data-editor-class' => $editorClass,
|
||||
'data-token-callback' => 'email:getBuilderTokens',
|
||||
'data-token-activator' => '{',
|
||||
],
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'dwc';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Helper;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\DynamicContentBundle\DynamicContentEvents;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Event\ContactFiltersEvaluateEvent;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Mautic\EmailBundle\EventListener\MatchFilterForLeadTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class DynamicContentHelper
|
||||
{
|
||||
use MatchFilterForLeadTrait;
|
||||
|
||||
/**
|
||||
* @const DYNAMIC_CONTENT_REGEX
|
||||
*/
|
||||
public const DYNAMIC_CONTENT_REGEX = '/{(dynamiccontent)=(\w+)(?:\/}|}(?:([^{]*(?:{(?!\/\1})[^{]*)*){\/\1})?)/is';
|
||||
|
||||
/**
|
||||
* @const DYNAMIC_WEB_CONTENT_REGEX
|
||||
*/
|
||||
public const DYNAMIC_WEB_CONTENT_REGEX = '/{dwc=(.*?)}/';
|
||||
|
||||
public function __construct(
|
||||
protected DynamicContentModel $dynamicContentModel,
|
||||
protected RealTimeExecutioner $realTimeExecutioner,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected LeadModel $leadModel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slot
|
||||
* @param Lead|array $lead
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDynamicContentForLead($slot, $lead)
|
||||
{
|
||||
// Attempt campaign slots first
|
||||
$dwcActionResponse = $this->realTimeExecutioner->execute('dwc.decision', $slot, 'dynamicContent')->getActionResponses('dwc.push_content');
|
||||
if (!empty($dwcActionResponse)) {
|
||||
return array_shift($dwcActionResponse);
|
||||
}
|
||||
|
||||
// Attempt stored content second
|
||||
$data = $this->dynamicContentModel->getSlotContentForLead($slot, $lead);
|
||||
if (!empty($data)) {
|
||||
$content = $data['content'];
|
||||
$dwc = $this->dynamicContentModel->getEntity($data['id']);
|
||||
if ($dwc instanceof DynamicContent) {
|
||||
$content = $this->getRealDynamicContent($slot, $lead, $dwc);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Finally attempt standalone DWC
|
||||
return $this->getDynamicContentSlotForLead($slot, $lead);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slotName
|
||||
* @param Lead|array $lead
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDynamicContentSlotForLead($slotName, $lead)
|
||||
{
|
||||
$leadArray = [];
|
||||
if ($lead instanceof Lead) {
|
||||
$leadArray = $this->convertLeadToArray($lead);
|
||||
}
|
||||
|
||||
$dwcs = $this->getDwcsBySlotName($slotName, true);
|
||||
/** @var DynamicContent $dwc */
|
||||
foreach ($dwcs as $dwc) {
|
||||
if ($dwc->getIsCampaignBased()) {
|
||||
continue;
|
||||
}
|
||||
if ($lead && $this->filtersMatchContact($dwc->getFilters(), $leadArray)) {
|
||||
return $lead ? $this->getRealDynamicContent($dwc->getSlotName(), $lead, $dwc) : '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param Lead|array $lead
|
||||
*
|
||||
* @return string Content with the {content} tokens replaced with dynamic content
|
||||
*/
|
||||
public function replaceTokensInContent($content, $lead)
|
||||
{
|
||||
// Find all dynamic content tags
|
||||
preg_match_all(self::DYNAMIC_CONTENT_REGEX, $content, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$slot = $match[2];
|
||||
$defaultContent = $match[3];
|
||||
|
||||
$dwcContent = $this->getDynamicContentForLead($slot, $lead);
|
||||
|
||||
if (!$dwcContent) {
|
||||
$dwcContent = $defaultContent;
|
||||
}
|
||||
|
||||
$content = str_replace($matches[0], $dwcContent, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param Lead|null $lead
|
||||
*/
|
||||
public function findDwcTokens($content, $lead): array
|
||||
{
|
||||
preg_match_all(self::DYNAMIC_WEB_CONTENT_REGEX, $content, $matches);
|
||||
|
||||
$tokens = [];
|
||||
if (!empty($matches[1])) {
|
||||
foreach ($matches[1] as $key => $slotName) {
|
||||
$token = $matches[0][$key];
|
||||
if (!empty($tokens[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dwcs = $this->getDwcsBySlotName($slotName);
|
||||
|
||||
/** @var DynamicContent $dwc */
|
||||
foreach ($dwcs as $dwc) {
|
||||
if ($dwc->getIsCampaignBased()) {
|
||||
continue;
|
||||
}
|
||||
$content = $lead ? $this->getDynamicContentSlotForLead($dwc->getSlotName(), $lead) : '';
|
||||
$tokens[$token]['content'] = $content;
|
||||
$tokens[$token]['filters'] = $dwc->getFilters();
|
||||
}
|
||||
}
|
||||
|
||||
unset($matches);
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slot
|
||||
* @param Lead|mixed[] $lead
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRealDynamicContent($slot, $lead, DynamicContent $dwc)
|
||||
{
|
||||
$content = $dwc->getContent();
|
||||
// Determine a translation based on contact's preferred locale
|
||||
/** @var DynamicContent $translation */
|
||||
list($ignore, $translation) = $this->dynamicContentModel->getTranslatedEntity($dwc, $lead);
|
||||
if ($translation !== $dwc) {
|
||||
// Use translated version of content
|
||||
$dwc = $translation;
|
||||
$content = $dwc->getContent();
|
||||
}
|
||||
$stat = $this->dynamicContentModel->createStatEntry($dwc, $lead, $slot);
|
||||
|
||||
$tokenEvent = new TokenReplacementEvent($content, $lead, ['slot' => $slot, 'dynamic_content_id' => $dwc->getId()]);
|
||||
$tokenEvent->setStat($stat);
|
||||
$this->dispatcher->dispatch($tokenEvent, DynamicContentEvents::TOKEN_REPLACEMENT);
|
||||
|
||||
return $tokenEvent->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slotName
|
||||
* @param bool $publishedOnly
|
||||
*
|
||||
* @return array|\Doctrine\ORM\Tools\Pagination\Paginator
|
||||
*/
|
||||
public function getDwcsBySlotName($slotName, $publishedOnly = false)
|
||||
{
|
||||
$filter = [
|
||||
'where' => [
|
||||
[
|
||||
'col' => 'e.slotName',
|
||||
'expr' => 'eq',
|
||||
'val' => $slotName,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if ($publishedOnly) {
|
||||
$filter['where'][] = [
|
||||
'col' => 'e.isPublished',
|
||||
'expr' => 'eq',
|
||||
'val' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->dynamicContentModel->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
'ignore_paginator' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
*/
|
||||
public function convertLeadToArray($lead): array
|
||||
{
|
||||
return array_merge(
|
||||
$lead->getProfileFields(),
|
||||
[
|
||||
'tags' => array_map(
|
||||
fn (Tag $v) => $v->getId(),
|
||||
$lead->getTags()->toArray()
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
* @param mixed[] $contactArray
|
||||
*/
|
||||
private function filtersMatchContact(array $filters, array $contactArray): bool
|
||||
{
|
||||
if (empty($contactArray['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We attempt even listeners first
|
||||
if ($this->dispatcher->hasListeners(DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE)) {
|
||||
/** @var Lead $contact */
|
||||
$contact = $this->leadModel->getEntity($contactArray['id']);
|
||||
|
||||
$event = new ContactFiltersEvaluateEvent($filters, $contact);
|
||||
$this->dispatcher->dispatch($event, DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE);
|
||||
if ($event->isMatch()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->matchFilterForLead($filters, $contactArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticDynamicContentBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Model;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Model\TranslationModelTrait;
|
||||
use Mautic\CoreBundle\Model\VariantModelTrait;
|
||||
use Mautic\DynamicContentBundle\DynamicContentEvents;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContentRepository;
|
||||
use Mautic\DynamicContentBundle\Entity\Stat;
|
||||
use Mautic\DynamicContentBundle\Event\DynamicContentEvent;
|
||||
use Mautic\DynamicContentBundle\Form\Type\DynamicContentType;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<DynamicContent>
|
||||
*
|
||||
* @implements AjaxLookupModelInterface<DynamicContent>
|
||||
*/
|
||||
class DynamicContentModel extends FormModel implements AjaxLookupModelInterface, GlobalSearchInterface
|
||||
{
|
||||
use VariantModelTrait;
|
||||
use TranslationModelTrait;
|
||||
|
||||
/**
|
||||
* Retrieve the permissions base.
|
||||
*/
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'dynamiccontent:dynamiccontents';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DynamicContentRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
/** @var DynamicContentRepository $repo */
|
||||
$repo = $this->em->getRepository(DynamicContent::class);
|
||||
|
||||
$repo->setTranslator($this->translator);
|
||||
$repo->setCurrentUser($this->userHelper->getUser());
|
||||
|
||||
return $repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\DynamicContentBundle\Entity\StatRepository
|
||||
*/
|
||||
public function getStatRepository()
|
||||
{
|
||||
return $this->em->getRepository(Stat::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param bool $unlock
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
parent::saveEntity($entity, $unlock);
|
||||
|
||||
$this->postTranslationEntitySave($entity);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?DynamicContent
|
||||
{
|
||||
if (null === $id) {
|
||||
return new DynamicContent();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
public function checkEntityBySlotName(string $slotName, ?string $type = null, string $typeCondition = '=',
|
||||
?int $skipId = null): bool
|
||||
{
|
||||
$qb = $this->em->getConnection()->createQueryBuilder();
|
||||
|
||||
$qb->select('1')
|
||||
->from(MAUTIC_TABLE_PREFIX.'dynamic_content')
|
||||
->where($qb->expr()->eq('slot_name', ':slot_name'))
|
||||
->setParameter('slot_name', $slotName)
|
||||
->setMaxResults(1);
|
||||
|
||||
if (!empty($type)) {
|
||||
if (!in_array($typeCondition, ['=', '<>', '!='], true)) {
|
||||
throw new \InvalidArgumentException("Invalid operator '$typeCondition'");
|
||||
}
|
||||
|
||||
$qb->andWhere("type {$typeCondition} :type");
|
||||
$qb->setParameter('type', $type);
|
||||
}
|
||||
|
||||
if (null !== $skipId) {
|
||||
$qb->andWhere('id != :id');
|
||||
$qb->setParameter('id', $skipId);
|
||||
}
|
||||
|
||||
return (bool) $qb->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $action
|
||||
* @param array $options
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): FormInterface
|
||||
{
|
||||
if (!$entity instanceof DynamicContent) {
|
||||
throw new \InvalidArgumentException('Entity must be of class DynamicContent');
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(DynamicContentType::class, $entity, $options);
|
||||
}
|
||||
|
||||
public function setSlotContentForLead(DynamicContent $dwc, Lead $lead, $slot): void
|
||||
{
|
||||
$qb = $this->em->getConnection()->createQueryBuilder();
|
||||
|
||||
$qb->insert(MAUTIC_TABLE_PREFIX.'dynamic_content_lead_data')
|
||||
->values([
|
||||
'lead_id' => $lead->getId(),
|
||||
'dynamic_content_id' => $dwc->getId(),
|
||||
'slot' => ':slot',
|
||||
'date_added' => $qb->expr()->literal((new \DateTime())->format('Y-m-d H:i:s')),
|
||||
])->setParameter('slot', $slot);
|
||||
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slot
|
||||
* @param Lead|array $lead
|
||||
*
|
||||
* @return array<string, mixed>|false
|
||||
*/
|
||||
public function getSlotContentForLead($slot, $lead)
|
||||
{
|
||||
if (!$lead) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->em->getConnection()->createQueryBuilder();
|
||||
|
||||
$id = $lead instanceof Lead ? $lead->getId() : $lead['id'];
|
||||
|
||||
$qb->select('dc.id, dc.content')
|
||||
->from(MAUTIC_TABLE_PREFIX.'dynamic_content', 'dc')
|
||||
->leftJoin('dc', MAUTIC_TABLE_PREFIX.'dynamic_content_lead_data', 'dcld', 'dcld.dynamic_content_id = dc.id')
|
||||
->andWhere($qb->expr()->eq('dcld.slot', ':slot'))
|
||||
->andWhere($qb->expr()->eq('dcld.lead_id', ':lead_id'))
|
||||
->andWhere($qb->expr()->eq('dc.is_published', 1))
|
||||
->setParameter('slot', $slot)
|
||||
->setParameter('lead_id', $id)
|
||||
->orderBy('dcld.date_added', 'DESC')
|
||||
->addOrderBy('dcld.id', 'DESC');
|
||||
|
||||
return $qb->executeQuery()->fetchAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead|array $lead
|
||||
* @param string $source
|
||||
*
|
||||
* @return Stat|null
|
||||
*/
|
||||
public function createStatEntry(DynamicContent $dynamicContent, $lead, $source = null)
|
||||
{
|
||||
if (empty($lead)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($lead instanceof Lead && !$lead->getId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($lead)) {
|
||||
if (empty($lead['id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lead = $this->em->getReference(Lead::class, $lead['id']);
|
||||
}
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setDateSent(new \DateTime());
|
||||
$stat->setLead($lead);
|
||||
$stat->setDynamicContent($dynamicContent);
|
||||
$stat->setSource($source);
|
||||
|
||||
$this->getStatRepository()->saveEntity($stat);
|
||||
|
||||
return $stat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof DynamicContent) {
|
||||
throw new MethodNotAllowedHttpException(['Dynamic Content']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = DynamicContentEvents::PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = DynamicContentEvents::POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = DynamicContentEvents::PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = DynamicContentEvents::POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new DynamicContentEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the page table and limits created_by to currently logged in user.
|
||||
*/
|
||||
public function limitQueryToCreator(QueryBuilder &$q): void
|
||||
{
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'dynamic_content', 'd', 'd.id = t.dynamic_content_id')
|
||||
->andWhere('d.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of hits.
|
||||
*
|
||||
* @param char $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getHitsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$flag = null;
|
||||
|
||||
if (isset($filter['flag'])) {
|
||||
$flag = $filter['flag'];
|
||||
unset($filter['flag']);
|
||||
}
|
||||
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
|
||||
if (!$flag || 'total_and_unique' === $flag) {
|
||||
$q = $query->prepareTimeDataQuery('dynamic_content_stats', 'date_sent', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.dynamicContent.show.total.views'), $data);
|
||||
}
|
||||
|
||||
if ('unique' === $flag || 'total_and_unique' === $flag) {
|
||||
$q = $query->prepareTimeDataQuery('dynamic_content_stats', 'date_sent', $filter);
|
||||
$q->groupBy('t.lead_id, t.date_sent');
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.dynamicContent.show.unique.views'), $data);
|
||||
}
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param array $options
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = []): array
|
||||
{
|
||||
$results = [];
|
||||
switch ($type) {
|
||||
case 'dynamicContent':
|
||||
$entities = $this->getRepository()->getDynamicContentList(
|
||||
$filter,
|
||||
$limit,
|
||||
$start,
|
||||
$this->security->isGranted($this->getPermissionBase().':viewother'),
|
||||
$options['top_level'] ?? false,
|
||||
$options['ignore_ids'] ?? [],
|
||||
$options['where'] ?? ''
|
||||
);
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$results[$entity['language']][$entity['id']] = $entity['name'];
|
||||
}
|
||||
|
||||
// sort by language
|
||||
ksort($results);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% set troubleshootingContent %}
|
||||
<div class='dwc--generator-troubleshooting'>
|
||||
<i class='ri-close-circle-line ri-xl mb-sm mt-lg d-inline-flex'></i>
|
||||
<h5 class='fw-sb mb-xs'>{{ 'mautic.dynamiccontent.generator.troubleshooting.content_not_displaying'|trans }}</h5>
|
||||
<p>{{ 'mautic.dynamiccontent.generator.troubleshooting.content_not_displaying_text'|trans }}</p>
|
||||
|
||||
<i class='ri-javascript-line ri-xl mb-sm mt-lg d-inline-flex'></i>
|
||||
<h5 class='fw-sb mb-xs'>{{ 'mautic.dynamiccontent.generator.troubleshooting.javascript'|trans }}</h5>
|
||||
<p>{{ 'mautic.dynamiccontent.generator.troubleshooting.javascript_text'|trans }}</p>
|
||||
|
||||
<i class='ri-input-field ri-xl mb-sm mt-lg d-inline-flex'></i>
|
||||
<h5 class='fw-sb mb-xs'>{{ 'mautic.dynamiccontent.generator.troubleshooting.default_content'|trans }}</h5>
|
||||
<p>{{ 'mautic.dynamiccontent.generator.troubleshooting.default_content_text'|trans }}</p>
|
||||
|
||||
<i class='ri-server-line ri-xl mb-sm mt-lg d-inline-flex'></i>
|
||||
<h5 class='fw-sb mb-xs'>{{ 'mautic.dynamiccontent.generator.troubleshooting.cache'|trans }}</h5>
|
||||
<p>{{ 'mautic.dynamiccontent.generator.troubleshooting.cache_text'|trans }}</p>
|
||||
</div>
|
||||
{% endset %}
|
||||
<div>
|
||||
{% include '@MauticCore/Components/toggletip.html.twig' with {
|
||||
label: 'mautic.core.troubleshooting',
|
||||
size: 'xs',
|
||||
icon: 'ri-question-answer-line',
|
||||
content: troubleshootingContent,
|
||||
html: true,
|
||||
trigger: 'click'
|
||||
} %}
|
||||
</div>
|
||||
@@ -0,0 +1,150 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('app/bundles/DynamicContentBundle/Assets/css/dwc-generator.css') }}">
|
||||
|
||||
{% set htmlcode %}
|
||||
<pre class="dwc--generator-content-code d-flex fd-column fg-1 gap-md pa-0 fs-16 text-helper">{{ '<div
|
||||
data-slot="dwc"
|
||||
data-param-slot-name="' ~ entity.slotName ~ '">'|raw }}<div class="dwc--generator-content-editable layer-two d-inline-flex gap-4 ai-center ml-lg text-primary"><i class="ri-pencil-line ri-sm" title="{{ 'mautic.dynamiccontent.generator.default_content_tooltip'|trans }}" data-toggle="tooltip"></i><input class="form-control dwc--generator-content-input" type="text" placeholder="{{ 'mautic.dynamiccontent.generator.input_placeholder'|trans }}"></div>{{ '</div>'|escape }}</pre>
|
||||
{% endset %}
|
||||
|
||||
<div class="row mt-lg">
|
||||
<div class="col-xs-12">
|
||||
<h2 class="mt-lg mb-md">{{ 'mautic.dynamiccontent.generator.deployment_options'|trans }}</h2>
|
||||
<p class="text-muted mb-lg">{{ 'mautic.dynamiccontent.generator.deployment_instructions'|trans }}</p>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<ul class="list-group" role="tablist">
|
||||
<li role="presentation" class="list-group-item active">
|
||||
<a href="#dwc--generator-html" aria-controls="dwc--generator-html" role="tab" data-toggle="tab">
|
||||
<div class="d-flex jc-space-between mb-sm">
|
||||
<h4 class="list-group-item-heading fw-sb">{{ 'mautic.dynamiccontent.generator.html'|trans }}</h4>
|
||||
<i class="ri-html5-fill ri-lg"></i>
|
||||
</div>
|
||||
<p class="list-group-item-text">{{ 'mautic.dynamiccontent.generator.html_description'|trans }}</p>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="list-group-item">
|
||||
<a href="#dwc--generator-plugins" aria-controls="dwc--generator-plugins" role="tab" data-toggle="tab">
|
||||
<div class="d-flex jc-space-between mb-sm">
|
||||
<h4 class="list-group-item-heading fw-sb">{{ 'mautic.dynamiccontent.generator.plugins'|trans }}</h4>
|
||||
<i class="ri-apps-2-add-fill ri-lg"></i>
|
||||
</div>
|
||||
<p class="list-group-item-text">{{ 'mautic.dynamiccontent.generator.plugins_description'|trans }}</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ include('@MauticDynamicContent/DynamicContent/StandaloneSlotGenerator/dwc-generator--help.html.twig') }}
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-8 tab-content">
|
||||
|
||||
<div role="tabpanel" class="tab-pane fade in active" id="dwc--generator-html">
|
||||
<div class="tile dwc--generator-wrapper">
|
||||
<div class="d-flex fg-1">
|
||||
<div class="dwc--generator-toolbar pa-xs layer-two d-flex fd-column jc-space-between ai-start mr-md">
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
icon: 'ri-code-s-slash-line ri-lg',
|
||||
label: 'mautic.dynamiccontent.generator.switch_html_tag',
|
||||
icon_only: true,
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
attributes: {
|
||||
'class': 'ma-0',
|
||||
'type': 'button'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ri-question-line ri-lg',
|
||||
label: 'mautic.dynamiccontent.generator.more_information.html',
|
||||
icon_only: true,
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
attributes: {
|
||||
'class': 'ma-0',
|
||||
'type': 'button',
|
||||
'data-toggle': 'popover',
|
||||
'title': 'mautic.dynamiccontent.generator.html_tag_title'|trans,
|
||||
'data-content': 'mautic.dynamiccontent.generator.html_tag_info'|trans,
|
||||
'data-container': 'body',
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
|
||||
</div>
|
||||
<div class="dwc--generator-content mt-md mb-sm d-flex fd-column fg-1">
|
||||
{{ htmlcode }}
|
||||
<div class="dwc--generator-content-help text-helper small">
|
||||
<i class="ri-information-2-line"></i>
|
||||
{{ 'mautic.dynamiccontent.generator.preview_content_discard_notice'|trans }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="tabpanel" class="tab-pane fade" id="dwc--generator-plugins">
|
||||
<div class="tile dwc--generator-wrapper">
|
||||
<div class="d-flex fg-1">
|
||||
<div class="dwc--generator-toolbar pa-xs layer-two d-flex fd-column jc-space-between ai-start mr-md">
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
icon: 'ri-brackets-line ri-lg',
|
||||
label: 'mautic.dynamiccontent.generator.switch_code_wrapper',
|
||||
icon_only: true,
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
attributes: {
|
||||
'class': 'ma-0',
|
||||
'type': 'button'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ri-question-line ri-lg',
|
||||
label: 'mautic.dynamiccontent.generator.more_information.plugins',
|
||||
icon_only: true,
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
attributes: {
|
||||
'class': 'ma-0',
|
||||
'data-toggle': 'popover',
|
||||
'title': 'mautic.dynamiccontent.generator.when_to_change_brackets'|trans,
|
||||
'data-content': 'mautic.dynamiccontent.generator.joomla_syntax_info'|trans,
|
||||
'data-container': 'body',
|
||||
'type': 'button'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
|
||||
</div>
|
||||
<div class="dwc--generator-content mt-md mb-sm d-flex fd-column fg-1">
|
||||
<pre class="dwc--generator-content-code d-flex fd-column fg-1 gap-md pa-0 fs-16 text-helper">{mautic type="content" slot="{{ entity.slotName }}"} <div class="dwc--generator-content-editable layer-two d-inline-flex gap-4 ai-center ml-lg text-primary"> <i class="ri-pencil-line ri-sm" title="{{ 'mautic.dynamiccontent.generator.default_content_tooltip'|trans }}" data-toggle="tooltip"></i><input class="form-control dwc--generator-content-input" type="text" placeholder="{{ 'mautic.dynamiccontent.generator.input_placeholder'|trans }}"></div>{/mautic}
|
||||
</pre>
|
||||
<div class="dwc--generator-content-help text-helper small">
|
||||
<i class="ri-information-2-line"></i>
|
||||
{{ 'mautic.dynamiccontent.generator.preview_content_discard_notice'|trans }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.dynamiccontent.generator.copy_dynamic_content_slot',
|
||||
icon: 'ri-clipboard-line',
|
||||
variant: 'secondary',
|
||||
size: 'xl',
|
||||
wide: 'true',
|
||||
attributes: {
|
||||
'class': 'dwc--generator-copy'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
{#
|
||||
Variables
|
||||
- searchValue
|
||||
- items (\Mautic\DynamicContentBundle\Entity\DynamicContent[])
|
||||
- categories
|
||||
- page
|
||||
- limit
|
||||
- permissions
|
||||
- model
|
||||
- tmpl
|
||||
#}
|
||||
{% if items|length > 0 %}
|
||||
<div class="table-responsive page-list">
|
||||
<table class="table table-hover dwctable-list" id="dwcTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall': 'true',
|
||||
'target': '#dwcTable',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'dynamicContent',
|
||||
'orderBy': 'e.name',
|
||||
'text': 'mautic.core.name',
|
||||
'class': 'col-dwc-name',
|
||||
'default': true,
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'dynamicContent',
|
||||
'orderBy': 'e.slotName',
|
||||
'text': 'mautic.dynamicContent.label.slot_name',
|
||||
'class': 'col-dwc-slotname visible-md visible-lg',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'dynamicContent',
|
||||
'orderBy': 'c.title',
|
||||
'text': 'mautic.core.category',
|
||||
'class': 'col-dwc-category visible-md visible-lg',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'dynamicContent',
|
||||
'orderBy': 'e.id',
|
||||
'text': 'mautic.core.id',
|
||||
'class': 'col-dwc-id visible-md visible-lg',
|
||||
}) }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ include('@MauticCore/Helper/list_actions.html.twig', {
|
||||
'item': item,
|
||||
'templateButtons': {
|
||||
'edit': securityHasEntityAccess(permissions['dynamiccontent:dynamiccontents:editown'], permissions['dynamiccontent:dynamiccontents:editother'], item.createdBy),
|
||||
'clone': permissions['dynamiccontent:dynamiccontents:create'],
|
||||
'delete': securityHasEntityAccess(permissions['dynamiccontent:dynamiccontents:deleteown'], permissions['dynamiccontent:dynamiccontents:deleteother'], item.createdBy),
|
||||
},
|
||||
'routeBase': 'dynamicContent',
|
||||
'nameGetter': 'getName',
|
||||
}) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {'item': item, 'model': 'dynamicContent'}) }}
|
||||
<a href="{{ url('mautic_dynamicContent_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
{{ item.name }}
|
||||
{% if item.isVariant or item.isTranslation or item.isCampaignBased %}
|
||||
<span>
|
||||
{% if item.isVariant %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.ab_test'|trans }}"><i class="ri-fw ri-organization-chart"></i></span>
|
||||
{% endif %}
|
||||
{% if item.isTranslation %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.translation'|trans }}"><i class="ri-fw ri-translate"></i></span>
|
||||
{% endif %}
|
||||
{% if item.isCampaignBased %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.dwc.form.is.campaign.tooltip'|trans }}"><i class="ri-fw ri-megaphone-line"></i></span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{{ customContent('dynamiccontent.name', _context) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.slotName }}</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.category}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.id }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
{{ include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems': items|length,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'menuLinkId': 'mautic_dynamicContent_index',
|
||||
'baseUrl': url('mautic_dynamicContent_index'),
|
||||
'sessionVar': 'dynamicContent',
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ include('@MauticCore/Helper/noresults.html.twig') }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,214 @@
|
||||
{#
|
||||
#}
|
||||
{% extends (not isEmbedded) ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent 'dynamicContent' %}
|
||||
|
||||
{% block headerTitle entity.name %}
|
||||
|
||||
{% block publishStatus %}
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': entity,
|
||||
'status': 'available'
|
||||
}) -}}
|
||||
|
||||
{% set tags = [
|
||||
{
|
||||
color: 'warm-gray',
|
||||
label: entity.isCampaignBased
|
||||
? 'mautic.dwc.form.is.campaign'
|
||||
: 'mautic.dwc.form.is.standalone'
|
||||
}
|
||||
] %}
|
||||
|
||||
{% set tags = tags
|
||||
|merge(entity.isTranslation and not entity.isTranslation(true)
|
||||
? [{ color: 'warm-gray', label: 'mautic.core.icon_tooltip.translation' }]
|
||||
: [])
|
||||
|merge(entity.isTranslation(true)
|
||||
? [{ color: 'warm-gray', label: 'mautic.core.translation_of'|trans({'%parent%' : translations.parent.getName()}), icon: 'ri-translate' }]
|
||||
: [])
|
||||
%}
|
||||
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with { tags: tags } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block preHeader %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'item' : entity,
|
||||
'templateButtons' : {
|
||||
'close' : securityHasEntityAccess(permissions['dynamiccontent:dynamiccontents:viewown'], permissions['dynamiccontent:dynamiccontents:viewother'], entity.createdBy),
|
||||
},
|
||||
'routeBase' : 'dynamicContent',
|
||||
'targetLabel' : 'mautic.dynamicContent.dynamicContents'|trans
|
||||
}
|
||||
) -}}
|
||||
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': entity.category}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'item': entity,
|
||||
'customButtons': customButtons|default([]),
|
||||
'templateButtons': {
|
||||
'edit': securityHasEntityAccess(permissions['dynamiccontent:dynamiccontents:editown'], permissions['dynamiccontent:dynamiccontents:editother'], entity.createdBy),
|
||||
'clone': permissions['dynamiccontent:dynamiccontents:create'],
|
||||
'delete': securityHasEntityAccess(permissions['dynamiccontent:dynamiccontents:deleteown'], permissions['dynamiccontent:dynamiccontents:deleteother'], entity.createdBy),
|
||||
},
|
||||
'routeBase': 'dynamicContent',
|
||||
}) -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- set translationContent = include('@MauticCore/Translation/index.html.twig', {
|
||||
'activeEntity': entity,
|
||||
'translations': translations,
|
||||
'model': 'dynamicContent',
|
||||
'actionRoute': 'mautic_dynamicContent_action',
|
||||
})|trim -%}
|
||||
{%- set showTranslations = translationContent is not empty -%}
|
||||
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- left section -->
|
||||
<div class="col-md-9 height-auto">
|
||||
<div>
|
||||
<!-- page detail header -->
|
||||
{% set description %}
|
||||
{% if entity.isVariant(true) %}
|
||||
<div class="small">
|
||||
<a href="{{ path('mautic_dynamicContent_action', {'objectAction': 'view', 'objectId': variants.parent.id}) }}" data-toggle="ajax">
|
||||
{{ 'mautic.core.variant_of'|trans({'%parent%': variants.parent.name}) }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if entity.isTranslation(true) %}
|
||||
<div class="small">
|
||||
<a href="{{ path('mautic_dynamicContent_action', {'objectAction': 'view', 'objectId': translations.parent.id}) }}" data-toggle="ajax">
|
||||
{{ 'mautic.core.translation_of'|trans({'%parent%': translations.parent.name}) }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not entity.isCampaignBased %}
|
||||
<div class="small">
|
||||
{{ 'mautic.dynamicContent.header.is_filter_based'|trans({'%slot%': entity.slotName}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endset %}
|
||||
|
||||
{% include '@MauticCore/Helper/description--expanded.html.twig' with { 'description': description } %}
|
||||
<!--/ page detail header -->
|
||||
<!-- page detail collapseable -->
|
||||
<div class="collapse pr-md pl-md" id="page-details">
|
||||
<div class="pr-md pl-md pb-md">
|
||||
<div class="panel shd-none mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<tbody>
|
||||
{{ include('@MauticCore/Helper/details.html.twig', {'entity': entity}) }}
|
||||
<tr>
|
||||
<td width="20%"><span class="fw-b textTitle">{{ 'mautic.dynamicContent.slot.campaign'|trans }}</span></td>
|
||||
<td>{{ entity.isCampaignBased ? 'Yes' : 'No' }}</td>
|
||||
</tr>
|
||||
{% if not entity.isCampaignBased %}
|
||||
<tr>
|
||||
<td width="20%"><span class="fw-b textTitle">{{ 'mautic.dynamicContent.label.slot_name'|trans }}</span></td>
|
||||
<td>{{ entity.slotName }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ page detail collapseable -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- page detail collapseable toggler -->
|
||||
<div class="hr-expand nm">
|
||||
<span data-toggle="tooltip" title="Detail">
|
||||
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse" data-target="#page-details">
|
||||
<span class="caret"></span>
|
||||
{{ 'mautic.core.details'|trans }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<!--/ page detail collapseable toggler -->
|
||||
|
||||
<!-- some stats -->
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-md-3 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<span class="ri-line-chart-fill"></span>
|
||||
{{ 'mautic.dynamicContent.views'|trans }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md-9 va-m">
|
||||
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm': dateRangeForm, 'class': 'pull-right'}) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex fd-column pt-0 pl-15 pb-15 pr-15 min-h-256">
|
||||
{{ include('@MauticCore/Helper/chart.html.twig', {'chartData': entityViews, 'chartType': 'line', 'chartHeight': 300}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if not entity.isCampaignBased %}
|
||||
{{ include('@MauticDynamicContent/DynamicContent/StandaloneSlotGenerator/dwc-generator.html.twig') }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ stats -->
|
||||
|
||||
{{ customContent('details.stats.graph.below', _context) }}
|
||||
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
<li class="active">
|
||||
<a href="#clicks-container" role="tab" data-toggle="tab">
|
||||
{{ 'mautic.trackable.click_counts'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% if showTranslations %}
|
||||
<li class>
|
||||
<a href="#translation-container" role="tab" data-toggle="tab">
|
||||
{{ 'mautic.core.translations'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
</div>
|
||||
<!-- start: tab-content -->
|
||||
<div class="tab-content pa-md">
|
||||
<div class="tab-pane active active bdr-w-0" id="clicks-container">
|
||||
{{ include('@MauticPage/Trackable/click_counts.html.twig', {'trackables': trackables, 'entity': entity, 'channel': 'dynamicContent'}) }}
|
||||
|
||||
</div>
|
||||
<!-- #translation-container -->
|
||||
{% if showTranslations %}
|
||||
<div class="tab-pane bdr-w-0" id="translation-container">
|
||||
{{ translationContent|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!--/ #translation-container -->
|
||||
</div>
|
||||
<!-- end: tab-content -->
|
||||
</div>
|
||||
<!--/ left section -->
|
||||
|
||||
<!-- right section -->
|
||||
<div class="col-md-3 bdr-l height-auto">
|
||||
<hr class="hr-w-2" style="width:50%">
|
||||
<!-- recent activity -->
|
||||
{{ include('@MauticCore/Helper/recentactivity.html.twig', {'logs': logs}) }}
|
||||
</div>
|
||||
<!--/ right section -->
|
||||
</div>
|
||||
|
||||
<!--/ end: box layout -->
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,196 @@
|
||||
{#
|
||||
Used to create and edit dynamic content
|
||||
#}
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% form_theme form '@MauticDynamicContent/FormTheme/Filter/layout.html.twig' %}
|
||||
|
||||
{% block mauticContent 'dynamicContent' %}
|
||||
{% block translationBase 'mautic.dynamicContent' %}
|
||||
|
||||
{% block headerTitle %}
|
||||
{% if form.vars.data.id is empty %}
|
||||
{{ 'mautic.dynamicContent.header.new'|trans }}
|
||||
{% else %}
|
||||
{{ 'mautic.dynamicContent.header.edit'|trans({'%name%': form.vars.data.name}) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- set fields = form.vars.fields -%}
|
||||
{%- set index = form.filters.vars.value|keys|length -%}
|
||||
{%- set hasMainErrors = formContainsErrors(form, ['filters']) -%}
|
||||
{%- set hasFilterErrors = formContainsErrors(form.filters) -%}
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="box-layout">
|
||||
<div class="col-md-9 height-auto">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
<li class="active">
|
||||
<a href="#details" role="tab" data-toggle="tab" class="{% if hasMainErrors %}text-danger{% endif %}">
|
||||
{{ 'mautic.core.details'|trans }}
|
||||
{% if hasMainErrors %}
|
||||
<i class="ri-alert-line"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="{% if form.vars.value.isCampaignBased or form.updateSelect is defined %}hide{% endif %}" id="dwcFiltersTab">
|
||||
<a href="#filters" role="tab" data-toggle="tab" class="{% if hasFilterErrors %}text-danger{% endif %}">
|
||||
{{ 'mautic.core.filters'|trans }}
|
||||
{% if hasFilterErrors %}
|
||||
<i class="ri-alert-line"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- start: tab-content -->
|
||||
<div class="tab-content pa-md">
|
||||
<div class="tab-pane fade in active bdr-w-0" id="details">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.name) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.content) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade bdr-w-0" id="filters">
|
||||
{% if hasFilterErrors and '' is not same as form_errors(form.filters) %}
|
||||
<div class="has-error alert alert-danger" role="alert" style="padding:5px 10px 0 10px;">
|
||||
{{ form_errors(form.filters) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="dwc-filter bdr-w-0" id="{{ form.vars.id }}">
|
||||
<div class="row">
|
||||
<div class="col-xs-7">
|
||||
<label>{{ 'Filters'|trans }}</label>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<div class="form-group">
|
||||
<div class="available-filters mb-md pl-0"
|
||||
data-prototype="{{ form_widget(form.filters.vars.prototype)|e }}"
|
||||
data-index="{{ index + 1 }}">
|
||||
<select class="chosen form-control" id="available_filters">
|
||||
<option value=""></option>
|
||||
{% for object, field in fields %}
|
||||
{% set icon = 'company' == object ? 'building' : 'user' %}
|
||||
<optgroup label="{{ ('mautic.lead.' ~ object)|trans }}">
|
||||
{% for value, params in field %}
|
||||
{% set list = params.properties.list|default([]) %}
|
||||
{% if 'boolean' is same as params.properties.type %}
|
||||
{% set choices = formFieldParseBooleanList(list)|reverse %}
|
||||
{% else %}
|
||||
{% set choices = leadFormFieldParseListForChoices(list) %}
|
||||
{% endif %}
|
||||
{% set callback = params.properties.callback|default('') %}
|
||||
{% set operators = params.operators %}
|
||||
<option value="{{ value|e }}"
|
||||
id="available_{{ object }}_{{ value }}"
|
||||
data-field-object="{{ object }}"
|
||||
data-field-type="{{ params.properties.type }}"
|
||||
data-field-list="{{ list|json_encode }}"
|
||||
data-field-callback="{{ callback }}"
|
||||
data-field-operators="{{ operators|json_encode }}"
|
||||
class="segment-filter {{ icon }}">{{ params.label|trans }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="selected-filters" id="dwc_filters" data-filter-container>
|
||||
{{ form_widget(form.filters) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 height-auto bdr-l">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
{{ form_row(form.type) }}
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.language) }}
|
||||
{{ form_row(form.translationParent) }}
|
||||
<div id="publishStatus">
|
||||
{{ form_row(form.isPublished) }}
|
||||
</div>
|
||||
{% if form.updateSelect is not defined %}
|
||||
{{ form_row(form.isCampaignBased) }}
|
||||
{% endif %}
|
||||
<div id="slotNameDiv" class="{% if form.vars.value.isCampaignBased %}hide{% endif %}">
|
||||
{{ form_row(form.slotName) }}
|
||||
</div>
|
||||
<hr/>
|
||||
{% include '@MauticCore/FormTheme/Fields/_utm_tags_fields.html.twig' %}
|
||||
<div class="hide">
|
||||
{{ form_row(form.publishUp) }}
|
||||
{{ form_row(form.publishDown) }}
|
||||
{{ form_rest(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
||||
{%- set templates = {
|
||||
'countries': 'country-template',
|
||||
'regions': 'region-template',
|
||||
'timezones': 'timezone-template',
|
||||
'select': 'select-template',
|
||||
'lists': 'leadlist-template',
|
||||
'campaign': 'campaign-template',
|
||||
'deviceTypes': 'device_type-template',
|
||||
'deviceBrands': 'device_brand-template',
|
||||
'deviceOs': 'device_os-template',
|
||||
'emails': 'lead_email_received-template',
|
||||
'tags': 'tags-template',
|
||||
'stage': 'stage-template',
|
||||
'locales': 'locale-template',
|
||||
'globalcategory': 'globalcategory-template',
|
||||
} -%}
|
||||
<div class="hide" id="templates">
|
||||
{% for dataKey, template in templates %}
|
||||
<select class="form-control not-chosen {{ template }}"
|
||||
{% if 'tags' == dataKey %}data-placeholder="{{ 'mautic.lead.tags.select_or_create'|trans }}"{% endif %}
|
||||
{% if 'tags' == dataKey %}data-no-results-text="{{ 'mautic.lead.tags.enter_to_create'|trans }}"{% endif %}
|
||||
{% if 'tags' == dataKey %}data-allow-add="true"{% endif %}
|
||||
{% if 'tags' == dataKey %}onchange="Mautic.createLeadTag(this)"{% endif %}
|
||||
name="dwc[filters][__name__][filter]"
|
||||
id="dwc_filters___name___filter">
|
||||
{% if form.vars[dataKey] is defined %}
|
||||
{% set index = 0 %}
|
||||
{% for label, value in form.vars[dataKey] %}
|
||||
{% if value is iterable %}
|
||||
<optgroup label="{{ label }}">
|
||||
{% for optionLabel, optionValue in value %}
|
||||
{% if (dataKey == 'regions') %}
|
||||
{% set optionValue = index %}
|
||||
{% set index = index + 1 %}
|
||||
{% endif %}
|
||||
<option value="{{ optionValue }}">{{ optionLabel }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% elseif 'lists' != dataKey or (currentListId is defined and value != currentListId) %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{#
|
||||
Variables
|
||||
- searchValue
|
||||
- items (\Mautic\DynamicContentBundle\Entity\DynamicContent[])
|
||||
- categories
|
||||
- page
|
||||
- limit
|
||||
- permissions
|
||||
- model
|
||||
- tmpl
|
||||
#}
|
||||
{%- set isIndex = 'index' == tmpl -%}
|
||||
{%- set tmpl = 'list' -%}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent 'dynamicContent' %}
|
||||
|
||||
{% block headerTitle 'mautic.dynamicContent.dynamicContents'|trans %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="panel panel-default">
|
||||
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'searchHelp': 'mautic.page.help.searchcommands',
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['dynamiccontent:dynamiccontents:create'],
|
||||
},
|
||||
'routeBase': 'dynamicContent',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'routeBase': 'dynamicContent',
|
||||
'templateButtons': {
|
||||
'delete': permissions['dynamiccontent:dynamiccontents:deleteown'] or permissions['dynamiccontent:dynamiccontents:deleteother'],
|
||||
},
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.available',
|
||||
'tooltip': 'mautic.core.searchcommand.ispublished.description',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.unavailable',
|
||||
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
|
||||
'icon': 'ri-close-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isuncategorized',
|
||||
'label': 'mautic.core.form.uncategorized',
|
||||
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
|
||||
'icon': 'ri-folder-unknow-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ismine',
|
||||
'label': 'mautic.core.searchcommand.ismine.label',
|
||||
'tooltip': 'mautic.core.searchcommand.ismine.description',
|
||||
'icon': 'ri-user-line'
|
||||
}
|
||||
]
|
||||
}) }}
|
||||
<div class="page-list">
|
||||
{{ include('@MauticDynamicContent/DynamicContent/_list.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ include('@MauticDynamicContent/DynamicContent/_list.html.twig') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% block _dynamiccontentdecision_list_row %}
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
{{ form_row(form.dynamicContent) }}
|
||||
</div>
|
||||
<div class="col-xs-4 mt-lg">
|
||||
<div class="mt-3">
|
||||
{{ form_row(form.newDynamicContentButton) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% block _dynamiccontentpush_list_row %}
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
{{ form_row(form.dynamicContent) }}
|
||||
</div>
|
||||
<div class="col-xs-4 mt-lg">
|
||||
<div class="mt-3">
|
||||
{{ form_row(form.newDynamicContentButton) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% block _dwc_filters_entry_widget %}
|
||||
{%- set isPrototype = '__name__' == form.vars.name -%}
|
||||
{%- set filterType = form.field.vars.value -%}
|
||||
{%- set inGroup = form.vars.data.glue is defined and 'and' is same as form.vars.data.glue -%}
|
||||
{%- set isBehavior = fields.behaviors[filterType].label is defined -%}
|
||||
{%- set icon = form.vars.data.object is defined and 'company' == form.vars.data.object ? 'ri-building-2-line' : 'ri-user-6-fill' -%}
|
||||
|
||||
{%- if isBehavior -%}
|
||||
{%- set object = 'behaviors' -%}
|
||||
{%- else -%}
|
||||
{%- set object = form.vars.data.object is defined ? form.vars.data.object : 'lead' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if isPrototype or fields[object][filterType].label is defined -%}
|
||||
<div class="panel {% if inGroup and false is same as first %}in-group{% endif %}">
|
||||
<div class="panel-heading {% if not isPrototype and '0' is same as form.vars.name %}hide{% endif %}">
|
||||
<div class="panel-glue col-sm-2 pl-0 ">
|
||||
{{ form_widget(form.glue) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-xs-6 col-sm-3 field-name">
|
||||
<i class="object-icon fa {{ icon }}" aria-hidden="true"></i>
|
||||
<span>{{ isPrototype ? '__label__' : fields[object][filterType].label }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-6 col-sm-3 padding-none">
|
||||
{{ form_widget(form.operator) }}
|
||||
</div>
|
||||
|
||||
{%- set hasErrors = form.filter.vars.errors|length or form.display.vars.errors|length -%}
|
||||
<div class="col-xs-10 col-sm-5 padding-none {% if hasErrors %}has-error{% endif %}">
|
||||
{{ form_widget(form.filter) }}
|
||||
{{ form_widget(form.display) }}
|
||||
{{ form_errors(form.filter) }}
|
||||
{{ form_errors(form.display) }}
|
||||
</div>
|
||||
|
||||
<div class="col-xs-2 col-sm-1">
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.report.report.label.removefilter',
|
||||
variant: 'ghost',
|
||||
icon_only: true,
|
||||
size: 'xs',
|
||||
danger: 'true',
|
||||
icon: 'ri-delete-bin-line',
|
||||
href: 'javascript: void(0);',
|
||||
attributes: {
|
||||
'class': 'remove-selected text-danger pull-right'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
{{ form_widget(form.field) }}
|
||||
{{ form_widget(form.type) }}
|
||||
{{ form_widget(form.object) }}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block _dwc_filters_widget %}
|
||||
{% use '@MauticLead/FormTheme/Filter/_leadlist_filters_widget.html.twig' %}
|
||||
{{ block('_leadlist_filters_widget') }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_dynamicContent_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="d-flex ai-center">
|
||||
<a class="fg-1" href="{{ url('mautic_dynamicContent_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': item.getPublishUp() is not null ? 'active' : 'available',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% set data = event.extra.stat.sentDetails %}
|
||||
|
||||
{% if data.failed is not defined and data.timeline is defined %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{{ 'mautic.dynamicContent.timeline.content'|trans }}</dt>
|
||||
<dd>{{ data.timeline|trans }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,27 @@
|
||||
<!-- Dynamic Content tokens -->
|
||||
<li class="panel">
|
||||
<a role="button" id="headingDynamicContentTokens" class="accordion-heading collapsed" data-toggle="collapse"
|
||||
data-parent="#tokensAccordion" href="#collapseDynamicContentTokens" aria-expanded="false"
|
||||
aria-controls="collapseDynamicContentTokens">
|
||||
<i class="ri-arrow-down-s-line accordion-arrow"></i>
|
||||
<span class="accordion-title">{{ 'mautic.placeholder_tokens.dynamic_content_tokens'|trans }}</span>
|
||||
</a>
|
||||
<div id="collapseDynamicContentTokens" class="collapse accordion-wrapper" role="tabpanel"
|
||||
aria-labelledby="headingDynamicContentTokens">
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'mautic.placeholder_tokens.variable_name'|trans }}</th>
|
||||
<th>{{ 'mautic.placeholder_tokens.variable_syntax'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'mautic.placeholder_tokens.dynamic_content.example'|trans }}</td>
|
||||
<td><code>{dynamiccontent="Dynamic Content 1"}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class DynamicContentPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct($params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
$this->addStandardPermissions('categories');
|
||||
$this->addExtendedPermissions('dynamiccontents');
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'dynamiccontent';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields('dynamiccontent', 'categories', $builder, $data);
|
||||
$this->addExtendedFormFields('dynamiccontent', 'dynamiccontents', $builder, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ClickthroughHelper;
|
||||
use Mautic\CoreBundle\Test\IsolatedTestTrait;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContentLeadData;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
|
||||
#[\PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses]
|
||||
class DynamicContentApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use IsolatedTestTrait;
|
||||
|
||||
public function testDwcGetEndpointForNoSlotNorContact(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/dwc/slot-a');
|
||||
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_NO_CONTENT, $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testDwcGetEndpointForASlotAndContact(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('johana@doe.email');
|
||||
|
||||
$dwc = new DynamicContent();
|
||||
$dwc->setContent('<some>content</some>');
|
||||
$dwc->setName('Slot A');
|
||||
$dwc->setSlotName('slot-a');
|
||||
|
||||
$dwcContact = new DynamicContentLeadData();
|
||||
$dwcContact->setDateAdded(new \DateTime());
|
||||
$dwcContact->setDynamicContent($dwc);
|
||||
$dwcContact->setLead($contact);
|
||||
$dwcContact->setSlot($dwc->getSlotName());
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setLead($contact);
|
||||
$stat->setTrackingHash('tracking-hash-1');
|
||||
$stat->setEmailAddress($contact->getEmail());
|
||||
$stat->setDateSent(new \DateTime());
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->persist($stat);
|
||||
$this->em->persist($dwc);
|
||||
$this->em->persist($dwcContact);
|
||||
$this->em->flush();
|
||||
|
||||
$ct = ClickthroughHelper::encodeArrayForUrl(['stat' => 'tracking-hash-1']);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/dwc/slot-a?ct={$ct}");
|
||||
|
||||
self::assertResponseIsSuccessful($this->client->getResponse()->getContent());
|
||||
|
||||
$responseArray = json_decode($this->client->getResponse()->getContent(), true);
|
||||
Assert::assertSame('<some>content</some>', $responseArray['content']);
|
||||
}
|
||||
|
||||
public function testCreateDwc(): void
|
||||
{
|
||||
$payload = [
|
||||
'name' => 'API test',
|
||||
'content' => 'API test',
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/dynamiccontents/new', $payload);
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_CREATED, $this->client->getResponse()->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Mautic\UserBundle\Entity\Permission;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class DynamicContentControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public const PERMISSION_CREATE = 'dynamiccontent:dynamiccontents:create';
|
||||
public const PERMISSION_DELETE_OTHER = 'dynamiccontent:dynamiccontents:deleteother';
|
||||
public const PERMISSION_DELETE_OWN = 'dynamiccontent:dynamiccontents:deleteown';
|
||||
|
||||
public const BITWISE_BY_PERM = [
|
||||
self::PERMISSION_CREATE => 52,
|
||||
self::PERMISSION_DELETE_OWN => 66,
|
||||
self::PERMISSION_DELETE_OTHER => 150,
|
||||
];
|
||||
private const NO_NESTING_VALIDATION_MESSAGE = 'DWC tokens cannot be used within another DWC. Please remove any DWC tokens from the content to proceed.';
|
||||
|
||||
public function testAccessControlNewAction(): void
|
||||
{
|
||||
$this->createAndLoginUser(self::PERMISSION_CREATE);
|
||||
$this->client->request(Request::METHOD_GET, '/s/dwc/new');
|
||||
|
||||
Assert::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testNoNestingValidationNewAction(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/dwc/new');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$this->submitFormAndAssertNoNestingValidation($crawler);
|
||||
}
|
||||
|
||||
public function testForbiddenNewAction(): void
|
||||
{
|
||||
$this->createAndLoginUser();
|
||||
$this->client->request(Request::METHOD_GET, '/s/dwc/new');
|
||||
|
||||
Assert::assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testNoNestingValidationEditAction(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/dwc/new');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$buttonCrawler = $crawler->selectButton('Save');
|
||||
$form = $buttonCrawler->form();
|
||||
$form->setValues([
|
||||
'dwc[name]' => 'Some name',
|
||||
'dwc[content]' => 'Some content',
|
||||
]);
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertStringNotContainsString(self::NO_NESTING_VALIDATION_MESSAGE, $crawler->text());
|
||||
Assert::assertStringContainsString('Edit Dynamic Content', $crawler->text());
|
||||
|
||||
$this->submitFormAndAssertNoNestingValidation($crawler);
|
||||
}
|
||||
|
||||
public function testAccessDeleteAction(): void
|
||||
{
|
||||
$this->createAndLoginUser(self::PERMISSION_DELETE_OWN);
|
||||
$this->client->request(Request::METHOD_POST, '/s/dwc/delete');
|
||||
|
||||
Assert::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testForbiddenDeleteAction(): void
|
||||
{
|
||||
$this->createAndLoginUser();
|
||||
$this->client->request('GET', '/s/dwc/delete');
|
||||
|
||||
Assert::assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testDwcWithProject(): void
|
||||
{
|
||||
$dynamicContent = new DynamicContent();
|
||||
$dynamicContent->setName('test');
|
||||
$this->em->persist($dynamicContent);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/dwc/edit/'.$dynamicContent->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['dwc[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedAsset = $this->em->find(DynamicContent::class, $dynamicContent->getId());
|
||||
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
|
||||
}
|
||||
|
||||
private function createAndLoginUser(?string $permission = null): User
|
||||
{
|
||||
// Create non-admin role
|
||||
$role = $this->createRole();
|
||||
// Create permissions to update user for the role
|
||||
if (!empty($permission)) {
|
||||
$this->createPermission($permission, $role, self::BITWISE_BY_PERM[$permission]);
|
||||
}
|
||||
// Create non-admin user
|
||||
$user = $this->createUser($role);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($role);
|
||||
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createRole(bool $isAdmin = false): Role
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin($isAdmin);
|
||||
|
||||
$this->em->persist($role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
private function createPermission(string $rawPermission, Role $role, int $bitwise): void
|
||||
{
|
||||
$parts = explode(':', $rawPermission);
|
||||
$permission = new Permission();
|
||||
$permission->setBundle($parts[0]);
|
||||
$permission->setName($parts[1]);
|
||||
$permission->setRole($role);
|
||||
$permission->setBitwise($bitwise);
|
||||
|
||||
$this->em->persist($permission);
|
||||
}
|
||||
|
||||
private function createUser(Role $role): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setFirstName('John');
|
||||
$user->setLastName('Doe');
|
||||
$user->setUsername('john.doe');
|
||||
$user->setEmail('john.doe@email.com');
|
||||
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
\assert($hasher instanceof PasswordHasherInterface);
|
||||
$user->setPassword($hasher->hash('Maut1cR0cks!'));
|
||||
$user->setRole($role);
|
||||
|
||||
$this->em->persist($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function testIndexActionIsSuccessful(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/dwc');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testNewActionIsSuccessful(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/dwc/new');
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditActionIsSuccessful(): void
|
||||
{
|
||||
$entity = new DynamicContent();
|
||||
$entity->setName('Test Dynamic Content');
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/dwc/edit/'.$entity->getId());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testViewActionIsSuccessful(): void
|
||||
{
|
||||
$entity = new DynamicContent();
|
||||
$entity->setName('Test Dynamic Content');
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/dwc/view/'.$entity->getId());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
private function submitFormAndAssertNoNestingValidation(Crawler $crawler): void
|
||||
{
|
||||
$buttonCrawler = $crawler->selectButton('Save');
|
||||
$form = $buttonCrawler->form();
|
||||
$form->setValues([
|
||||
'dwc[name]' => 'Some name',
|
||||
'dwc[content]' => 'Some {dwc=slotname}',
|
||||
]);
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertStringContainsString(self::NO_NESTING_VALIDATION_MESSAGE, $crawler->text());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Controller;
|
||||
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class DynamicContentProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$dynamicContentAlpha = $this->createDynamicContent('DynamicContent Alpha');
|
||||
$dynamicContentBeta = $this->createDynamicContent('DynamicContent Beta');
|
||||
$this->createDynamicContent('DynamicContent Gamma');
|
||||
$this->createDynamicContent('DynamicContent Delta');
|
||||
|
||||
$dynamicContentAlpha->addProject($projectOne);
|
||||
$dynamicContentAlpha->addProject($projectTwo);
|
||||
$dynamicContentBeta->addProject($projectTwo);
|
||||
$dynamicContentBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/dynamiccontents', '/s/dwc']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['DynamicContent Alpha', 'DynamicContent Beta'],
|
||||
'unexpectedEntities' => ['DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND dynamicContent name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['DynamicContent Beta'],
|
||||
'unexpectedEntities' => ['DynamicContent Alpha', 'DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR dynamicContent name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['DynamicContent Alpha', 'DynamicContent Beta', 'DynamicContent Gamma'],
|
||||
'unexpectedEntities' => ['DynamicContent Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
'unexpectedEntities' => ['DynamicContent Alpha', 'DynamicContent Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['DynamicContent Beta'],
|
||||
'unexpectedEntities' => ['DynamicContent Alpha', 'DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
'unexpectedEntities' => ['DynamicContent Alpha', 'DynamicContent Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['DynamicContent Alpha', 'DynamicContent Beta'],
|
||||
'unexpectedEntities' => ['DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['DynamicContent Alpha', 'DynamicContent Gamma', 'DynamicContent Delta'],
|
||||
'unexpectedEntities' => ['DynamicContent Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createDynamicContent(string $name): DynamicContent
|
||||
{
|
||||
$dynamicContent = new DynamicContent();
|
||||
$dynamicContent->setName($name);
|
||||
$this->em->persist($dynamicContent);
|
||||
|
||||
return $dynamicContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Form\Type;
|
||||
|
||||
use DeviceDetector\Parser\Device\AbstractDeviceParser as DeviceParser;
|
||||
use DeviceDetector\Parser\OperatingSystem;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\DynamicContentBundle\DynamicContent\TypeList;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Form\Type\DynamicContentListType;
|
||||
use Mautic\DynamicContentBundle\Form\Type\DynamicContentType;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Helper\FormFieldHelper;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DynamicContentTypeTest extends TestCase
|
||||
{
|
||||
public function testFormBuild(): void
|
||||
{
|
||||
$entityManagerMock = $this->createMock(EntityManager::class);
|
||||
$listModelMock = $this->createMock(ListModel::class);
|
||||
$translatorInterfaceMock = $this->createMock(TranslatorInterface::class);
|
||||
$leadModelMock = $this->createMock(LeadModel::class);
|
||||
|
||||
$listModelMock->expects($this->once())
|
||||
->method('getChoiceFields')
|
||||
->willReturn($this->getMockChoiceFields());
|
||||
|
||||
$leadRepositoryMock = $this->createMock(LeadRepository::class);
|
||||
|
||||
$leadModelMock->expects($this->once())
|
||||
->method('getRepository')
|
||||
->willReturn($leadRepositoryMock);
|
||||
|
||||
$leadRepositoryMock->expects($this->once())
|
||||
->method('getCustomFieldList')
|
||||
->with('lead')
|
||||
->willReturn($this->getMockCustomFieldList());
|
||||
|
||||
$tags = $this->getMockTagList();
|
||||
$leadModelMock->expects($this->once())
|
||||
->method('getTagList')
|
||||
->willReturn($tags);
|
||||
|
||||
$dynamicContentType = new DynamicContentType(
|
||||
$entityManagerMock,
|
||||
$listModelMock,
|
||||
$translatorInterfaceMock,
|
||||
$leadModelMock,
|
||||
new TypeList(),
|
||||
);
|
||||
|
||||
$formBuilderInterfaceMock = $this->createMock(FormBuilderInterface::class);
|
||||
$options['data'] = new DynamicContent();
|
||||
|
||||
$tagChoices = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$tagChoices[$tag['value']] = $tag['label'];
|
||||
}
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$formBuilderInterfaceMock->expects($matcher)
|
||||
->method('create')->willReturnCallback(function (...$parameters) use ($matcher, $tagChoices, $formBuilderInterfaceMock) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('translationParent', $parameters[0]);
|
||||
$this->assertSame(DynamicContentListType::class, $parameters[1]);
|
||||
$this->assertSame([
|
||||
'label' => 'mautic.core.form.translation_parent',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.core.form.translation_parent.help',
|
||||
],
|
||||
'required' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'mautic.core.form.translation_parent.empty',
|
||||
'top_level' => 'translation',
|
||||
'ignore_ids' => [0 => 0],
|
||||
], $parameters[2]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('filters', $parameters[0]);
|
||||
$this->assertSame(CollectionType::class, $parameters[1]);
|
||||
$this->assertSame([
|
||||
'entry_type' => \Mautic\DynamicContentBundle\Form\Type\DwcEntryFiltersType::class,
|
||||
'entry_options' => [
|
||||
'countries' => FormFieldHelper::getCountryChoices(),
|
||||
'regions' => FormFieldHelper::getRegionChoices(),
|
||||
'timezones' => FormFieldHelper::getTimezonesChoices(),
|
||||
'locales' => FormFieldHelper::getLocaleChoices(),
|
||||
'fields' => $this->getMockChoiceFields(),
|
||||
'deviceTypes' => array_combine(
|
||||
DeviceParser::getAvailableDeviceTypeNames(),
|
||||
DeviceParser::getAvailableDeviceTypeNames()
|
||||
),
|
||||
'deviceBrands' => DeviceParser::$deviceBrands,
|
||||
'deviceOs' => array_combine(
|
||||
array_keys(OperatingSystem::getAvailableOperatingSystemFamilies()),
|
||||
array_keys(OperatingSystem::getAvailableOperatingSystemFamilies())
|
||||
),
|
||||
'tags' => $tagChoices,
|
||||
],
|
||||
'error_bubbling' => false,
|
||||
'mapped' => true,
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
], $parameters[2]);
|
||||
}
|
||||
|
||||
return $formBuilderInterfaceMock;
|
||||
});
|
||||
$matcher = $this->exactly(3);
|
||||
|
||||
$formBuilderInterfaceMock->expects($matcher)
|
||||
->method('addEventListener')->willReturnCallback(function (...$parameters) use ($matcher, $formBuilderInterfaceMock) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(FormEvents::PRE_SUBMIT, $parameters[0]);
|
||||
$callback = function ($listener) {
|
||||
$reflection = new \ReflectionFunction($listener);
|
||||
$parameters = $reflection->getParameters();
|
||||
|
||||
return FormEvent::class === (string) $parameters[0]->getType();
|
||||
};
|
||||
$this->assertTrue($callback($parameters[1]));
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(FormEvents::PRE_SET_DATA, $parameters[0]);
|
||||
$callback = function ($listener) {
|
||||
$reflection = new \ReflectionFunction($listener);
|
||||
$parameters = $reflection->getParameters();
|
||||
|
||||
return FormEvent::class === (string) $parameters[0]->getType();
|
||||
};
|
||||
$this->assertTrue($callback($parameters[1]));
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(FormEvents::POST_SUBMIT, $parameters[0]);
|
||||
$callback = function ($listener) {
|
||||
$reflection = new \ReflectionFunction($listener);
|
||||
$parameters = $reflection->getParameters();
|
||||
|
||||
return FormEvent::class === (string) $parameters[0]->getType();
|
||||
};
|
||||
$this->assertTrue($callback($parameters[1]));
|
||||
}
|
||||
|
||||
return $formBuilderInterfaceMock;
|
||||
});
|
||||
|
||||
$formBuilderInterfaceMock->expects($this->once())
|
||||
->method('get')
|
||||
->with('type')
|
||||
->willReturn($formBuilderInterfaceMock);
|
||||
|
||||
$dynamicContentType->buildForm($formBuilderInterfaceMock, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, array<string, mixed>>>
|
||||
*/
|
||||
private function getMockChoiceFields(): array
|
||||
{
|
||||
return [
|
||||
'lead' => [
|
||||
'email' => [
|
||||
'label' => 'Email',
|
||||
'properties' => ['type' => 'email'],
|
||||
'object' => 'lead',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
'empty' => 'empty',
|
||||
'not empty' => '!empty',
|
||||
'like' => 'like',
|
||||
'not like' => '!like',
|
||||
'regexp' => 'regexp',
|
||||
'not regexp' => '!regexp',
|
||||
'starts with' => 'startsWith',
|
||||
'ends with' => 'endsWith',
|
||||
'contains' => 'contains',
|
||||
],
|
||||
],
|
||||
'firstname' => [
|
||||
'label' => 'First Name',
|
||||
'properties' => ['type' => 'text'],
|
||||
'object' => 'lead',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
'empty' => 'empty',
|
||||
'not empty' => '!empty',
|
||||
'like' => 'like',
|
||||
'not like' => '!like',
|
||||
'regexp' => 'regexp',
|
||||
'not regexp' => '!regexp',
|
||||
'starts with' => 'startsWith',
|
||||
'ends with' => 'endsWith',
|
||||
'contains' => 'contains',
|
||||
],
|
||||
],
|
||||
'lastname' => [
|
||||
'label' => 'Last Name',
|
||||
'properties' => ['type' => 'text'],
|
||||
'object' => 'lead',
|
||||
'operators' => [
|
||||
'equals' => '=',
|
||||
'not equal' => '!=',
|
||||
'empty' => 'empty',
|
||||
'not empty' => '!empty',
|
||||
'like' => 'like',
|
||||
'not like' => '!like',
|
||||
'regexp' => 'regexp',
|
||||
'not regexp' => '!regexp',
|
||||
'starts with' => 'startsWith',
|
||||
'ends with' => 'endsWith',
|
||||
'contains' => 'contains',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, array<string,string|null>|string>>
|
||||
*/
|
||||
private function getMockCustomFieldList(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'firstname' => [
|
||||
'id' => '2',
|
||||
'label' => 'First Name',
|
||||
'alias' => 'firstname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => '1',
|
||||
'properties' => 'a:0:{}',
|
||||
'default_value' => null,
|
||||
],
|
||||
'lastname' => [
|
||||
'id' => '3',
|
||||
'label' => 'Last Name',
|
||||
'alias' => 'lastname',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => '1',
|
||||
'properties' => 'a:0:{}',
|
||||
'default_value' => null,
|
||||
],
|
||||
'email' => [
|
||||
'id' => '6',
|
||||
'label' => 'Email',
|
||||
'alias' => 'email',
|
||||
'type' => 'email',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'is_fixed' => '1',
|
||||
'properties' => 'a:0:{}',
|
||||
'default_value' => null,
|
||||
],
|
||||
],
|
||||
[
|
||||
'firstname' => 'firstname',
|
||||
'lastname' => 'lastname',
|
||||
'email' => 'email',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, string>>
|
||||
*/
|
||||
private function getMockTagList(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'value' => '1',
|
||||
'label' => 't1',
|
||||
],
|
||||
[
|
||||
'value' => '2',
|
||||
'label' => 't2',
|
||||
],
|
||||
[
|
||||
'value' => '3',
|
||||
'label' => 't3',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Helper\TokenHelper as AssetTokenHelper;
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\EventListener\DynamicContentSubscriber;
|
||||
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Mautic\FormBundle\Helper\TokenHelper as FormTokenHelper;
|
||||
use Mautic\LeadBundle\Entity\CompanyLeadRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Event\PageDisplayEvent;
|
||||
use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use MauticPlugin\MauticFocusBundle\Helper\TokenHelper as FocusTokenHelper;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class DynamicContentSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|TrackableModel
|
||||
*/
|
||||
private MockObject $trackableModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|PageTokenHelper
|
||||
*/
|
||||
private MockObject $pageTokenHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|AssetTokenHelper
|
||||
*/
|
||||
private MockObject $assetTokenHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|FormTokenHelper
|
||||
*/
|
||||
private MockObject $formTokenHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|FocusTokenHelper
|
||||
*/
|
||||
private MockObject $focusTokenHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|AuditLogModel
|
||||
*/
|
||||
private MockObject $auditLogModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|DynamicContentHelper
|
||||
*/
|
||||
private MockObject $dynamicContentHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|DynamicContentModel
|
||||
*/
|
||||
private MockObject $dynamicContentModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|CorePermissions
|
||||
*/
|
||||
private MockObject $security;
|
||||
|
||||
/**
|
||||
* @var MockObject|ContactTracker
|
||||
*/
|
||||
private MockObject $contactTracker;
|
||||
private \PHPUnit\Framework\MockObject\MockObject|CompanyLeadRepository $companyLeadRepositoryMock;
|
||||
|
||||
private DynamicContentSubscriber $subscriber;
|
||||
/**
|
||||
* @var CompanyModel|(CompanyModel&MockObject)|MockObject
|
||||
*/
|
||||
private MockObject $companyModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->trackableModel = $this->createMock(TrackableModel::class);
|
||||
$this->pageTokenHelper = $this->createMock(PageTokenHelper::class);
|
||||
$this->assetTokenHelper = $this->createMock(AssetTokenHelper::class);
|
||||
$this->formTokenHelper = $this->createMock(FormTokenHelper::class);
|
||||
$this->focusTokenHelper = $this->createMock(FocusTokenHelper::class);
|
||||
$this->auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->dynamicContentHelper = $this->createMock(DynamicContentHelper::class);
|
||||
$this->dynamicContentModel = $this->createMock(DynamicContentModel::class);
|
||||
$this->security = $this->createMock(CorePermissions::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->companyModel = $this->createMock(CompanyModel::class);
|
||||
$this->companyLeadRepositoryMock = $this->createMock(CompanyLeadRepository::class);
|
||||
$this->subscriber = new DynamicContentSubscriber(
|
||||
$this->trackableModel,
|
||||
$this->pageTokenHelper,
|
||||
$this->assetTokenHelper,
|
||||
$this->formTokenHelper,
|
||||
$this->focusTokenHelper,
|
||||
$this->auditLogModel,
|
||||
$this->dynamicContentHelper,
|
||||
$this->dynamicContentModel,
|
||||
$this->security,
|
||||
$this->contactTracker,
|
||||
$this->companyModel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test is ensuring this error won't happen again:.
|
||||
*
|
||||
* DOMDocumentFragment::appendXML(): Entity: line 1: parser error : xmlParseEntityRef: no name
|
||||
*
|
||||
* It happens when there is an ampersand in the DWC content.
|
||||
*/
|
||||
public function testDecodeTokensWithAmpersandDataAttribute(): void
|
||||
{
|
||||
$content = <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h2>Hello there!</h2>
|
||||
<div data-slot="dwc" data-param-slot-name="test-token"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
HTML;
|
||||
|
||||
$expected = <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h2>Hello there!</h2>
|
||||
<a href="https://john.doe&son">Link</a>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
HTML;
|
||||
$dwcContent = '<a href="https://john.doe&son">Link</a>';
|
||||
$event = $this->createMock(PageDisplayEvent::class);
|
||||
$contact = new Lead();
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn($content);
|
||||
|
||||
$this->security->expects($this->once())
|
||||
->method('isAnonymous')
|
||||
->willReturn(true);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($contact);
|
||||
|
||||
$this->dynamicContentHelper->expects($this->never())
|
||||
->method('convertLeadToArray');
|
||||
|
||||
$this->dynamicContentHelper->expects($this->once())
|
||||
->method('findDwcTokens')
|
||||
->with($content, $contact)
|
||||
->willReturn([]);
|
||||
|
||||
$this->dynamicContentHelper->expects($this->once())
|
||||
->method('getDynamicContentForLead')
|
||||
->with('test-token', $contact)
|
||||
->willReturn($dwcContent);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('setContent')
|
||||
->with($expected);
|
||||
|
||||
$this->subscriber->decodeTokens($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test is ensuring this error won't happen again:.
|
||||
*
|
||||
* DOMDocumentFragment::appendXML(): Entity: line 1: parser error : xmlParseEntityRef: no name
|
||||
*
|
||||
* It happens when there is an ampersand in the DWC content.
|
||||
*/
|
||||
public function testDecodeTokensWithAmpersandInlineDwc(): void
|
||||
{
|
||||
$content = <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h2>Hello there!</h2>
|
||||
{dwc=test-token}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
HTML;
|
||||
|
||||
$expected = <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h2>Hello there!</h2>
|
||||
<a href="https://john.doe&son">Link</a>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
HTML;
|
||||
$dwcContent = '<a href="https://john.doe&son">Link</a>';
|
||||
$event = $this->createMock(PageDisplayEvent::class);
|
||||
$contact = new Lead();
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn($content);
|
||||
|
||||
$this->security->expects($this->once())
|
||||
->method('isAnonymous')
|
||||
->willReturn(true);
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('getContact')
|
||||
->willReturn($contact);
|
||||
|
||||
$this->dynamicContentHelper->expects($this->never())
|
||||
->method('convertLeadToArray');
|
||||
|
||||
$this->dynamicContentHelper->expects($this->once())
|
||||
->method('findDwcTokens')
|
||||
->with($content, $contact)
|
||||
->willReturn([
|
||||
'{dwc=test-token}' => [
|
||||
'content' => $dwcContent,
|
||||
'filters' => [
|
||||
[
|
||||
'field' => 'email',
|
||||
'operator' => '!empty',
|
||||
'filter' => '',
|
||||
'type' => 'email',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->dynamicContentHelper->expects($this->never())
|
||||
->method('getDynamicContentForLead');
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('setContent')
|
||||
->with($expected);
|
||||
|
||||
$this->subscriber->decodeTokens($event);
|
||||
}
|
||||
|
||||
public function testOnTokenReplacement(): void
|
||||
{
|
||||
$content = <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h2>Hello there!</h2>
|
||||
Company name : {contactfield=companyname}
|
||||
Company Country : {contactfield=companycountry}
|
||||
Company website : {contactfield=companywebsite}
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
$expected = <<< HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h2>Hello there!</h2>
|
||||
Company name : Doe Corp
|
||||
Company Country : India
|
||||
Company website : https://www.doe.corp
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
$contact = $this->createMock(Lead::class);
|
||||
$event = $this->createMock(TokenReplacementEvent::class);
|
||||
|
||||
$event
|
||||
->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn($content);
|
||||
|
||||
$event
|
||||
->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($contact);
|
||||
|
||||
$event
|
||||
->expects($this->once())
|
||||
->method('getClickthrough')
|
||||
->willReturn([
|
||||
'slot' => 'slotOne',
|
||||
'dynamic_content_id' => 1,
|
||||
'lead' => 1,
|
||||
]);
|
||||
|
||||
$contact
|
||||
->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn([
|
||||
'id' => 1,
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'Doe',
|
||||
'company' => 'Doe Corp',
|
||||
'email' => 'john@doe.com',
|
||||
]);
|
||||
|
||||
$this->companyModel
|
||||
->expects($this->once())
|
||||
->method('getCompanyLeadRepository')
|
||||
->willReturn($this->companyLeadRepositoryMock);
|
||||
|
||||
$this->companyLeadRepositoryMock->expects($this->once())
|
||||
->method('getPrimaryCompanyByLeadId')
|
||||
->willReturn(
|
||||
[
|
||||
'id' => 1,
|
||||
'companyname' => 'Doe Corp',
|
||||
'companycountry' => 'India',
|
||||
'companywebsite' => 'https://www.doe.corp',
|
||||
'is_primary' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$this->pageTokenHelper
|
||||
->method('findPageTokens')
|
||||
->willReturn([]);
|
||||
$this->assetTokenHelper
|
||||
->method('findAssetTokens')
|
||||
->willReturn([]);
|
||||
$this->formTokenHelper
|
||||
->method('findFormTokens')
|
||||
->willReturn([]);
|
||||
$this->focusTokenHelper
|
||||
->method('findFocusTokens')
|
||||
->willReturn([]);
|
||||
|
||||
$this->trackableModel
|
||||
->method('parseContentForTrackables')
|
||||
->willReturn([
|
||||
$content,
|
||||
[],
|
||||
]);
|
||||
|
||||
$dwc = new DynamicContent();
|
||||
$dwc->setContent($content);
|
||||
|
||||
$this->dynamicContentModel
|
||||
->expects($this->once())
|
||||
->method('getEntity')
|
||||
->willReturn($dwc);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('setContent')
|
||||
->with($expected);
|
||||
|
||||
$this->subscriber->onTokenReplacement($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Unit\Helper;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\DynamicContentBundle\DynamicContentEvents;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Event\ContactFiltersEvaluateEvent;
|
||||
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
class DynamicContentHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&DynamicContentModel
|
||||
*/
|
||||
private MockObject $mockModel;
|
||||
|
||||
/**
|
||||
* @var MockObject&RealTimeExecutioner
|
||||
*/
|
||||
private MockObject $realTimeExecutioner;
|
||||
|
||||
/**
|
||||
* @var MockObject&EventDispatcher
|
||||
*/
|
||||
private MockObject $mockDispatcher;
|
||||
|
||||
/**
|
||||
* @var MockObject&LeadModel
|
||||
*/
|
||||
private MockObject $leadModel;
|
||||
|
||||
private DynamicContentHelper $helper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mockModel = $this->createMock(DynamicContentModel::class);
|
||||
$this->realTimeExecutioner = $this->createMock(RealTimeExecutioner::class);
|
||||
$this->mockDispatcher = $this->createMock(EventDispatcher::class);
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->helper = new DynamicContentHelper(
|
||||
$this->mockModel,
|
||||
$this->realTimeExecutioner,
|
||||
$this->mockDispatcher,
|
||||
$this->leadModel,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetDwcBySlotNameWithPublished(): void
|
||||
{
|
||||
$matcher = $this->exactly(2);
|
||||
$this->mockModel->expects($matcher)
|
||||
->method('getEntities')->willReturnCallback(function (...$parameters) use ($matcher) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame([
|
||||
'filter' => [
|
||||
'where' => [
|
||||
[
|
||||
'col' => 'e.slotName',
|
||||
'expr' => 'eq',
|
||||
'val' => 'test',
|
||||
],
|
||||
[
|
||||
'col' => 'e.isPublished',
|
||||
'expr' => 'eq',
|
||||
'val' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
'ignore_paginator' => true,
|
||||
], $parameters[0]);
|
||||
|
||||
return ['some entity'];
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame([
|
||||
'filter' => [
|
||||
'where' => [
|
||||
[
|
||||
'col' => 'e.slotName',
|
||||
'expr' => 'eq',
|
||||
'val' => 'secondtest',
|
||||
],
|
||||
],
|
||||
],
|
||||
'ignore_paginator' => true,
|
||||
], $parameters[0]);
|
||||
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// Only get published
|
||||
$this->assertCount(1, $this->helper->getDwcsBySlotName('test', true));
|
||||
|
||||
// Get all
|
||||
$this->assertCount(0, $this->helper->getDwcsBySlotName('secondtest'));
|
||||
}
|
||||
|
||||
public function testGetDynamicContentSlotForLeadWithListenerFindingMatch(): void
|
||||
{
|
||||
$slotName = 'test';
|
||||
$contact = new Lead();
|
||||
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
|
||||
|
||||
$slot = new DynamicContent();
|
||||
$slot->setName($slotName);
|
||||
$slot->setIsCampaignBased(false);
|
||||
// Setting filter that is not known to Mautic, but is for a plugin.
|
||||
$slot->setFilters([['field' => 'unicorn', 'type' => 'text', 'operator' => '=', 'filter' => 'magic']]);
|
||||
$slot->setContent('<p>test</p>');
|
||||
|
||||
$this->mockModel->method('getEntities')
|
||||
->willReturn([$slot]);
|
||||
|
||||
$this->mockModel->method('getTranslatedEntity')
|
||||
->willReturn([$slot, $slot]);
|
||||
|
||||
$this->leadModel->method('getEntity')
|
||||
->with(123)
|
||||
->willReturn($contact);
|
||||
|
||||
$this->mockDispatcher->method('hasListeners')->willReturn(true);
|
||||
$matcher = $this->exactly(2);
|
||||
$this->mockDispatcher->expects($matcher)
|
||||
->method('dispatch')->willReturnCallback(function (...$parameters) use ($matcher, $contact, $slot) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (ContactFiltersEvaluateEvent $event) use ($contact, $slot) {
|
||||
$this->assertSame($contact, $event->getContact());
|
||||
$this->assertSame($slot->getFilters(), $event->getFilters());
|
||||
|
||||
$event->setIsEvaluated(true);
|
||||
$event->setIsMatched(true); // Match found in a subscriber.
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE, $parameters[1]);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (TokenReplacementEvent $event) use ($contact, $slot) {
|
||||
$this->assertSame($contact, $event->getLead());
|
||||
$this->assertSame($slot->getContent(), $event->getContent());
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(DynamicContentEvents::TOKEN_REPLACEMENT, $parameters[1]);
|
||||
}
|
||||
|
||||
return $parameters[0];
|
||||
});
|
||||
|
||||
Assert::assertSame(
|
||||
'<p>test</p>',
|
||||
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetDynamicContentSlotForLeadWithListenerNotFindingMatch(): void
|
||||
{
|
||||
$slotName = 'test';
|
||||
$contact = new Lead();
|
||||
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
|
||||
|
||||
$slot = new DynamicContent();
|
||||
$slot->setName($slotName);
|
||||
$slot->setIsCampaignBased(false);
|
||||
// Setting filter that is not known to Mautic, nor any plugin.
|
||||
$slot->setFilters([['field' => 'unicorn', 'type' => 'text', 'operator' => '=', 'filter' => 'magic']]);
|
||||
$slot->setContent('<p>test</p>');
|
||||
|
||||
$this->mockModel->method('getEntities')
|
||||
->willReturn([$slot]);
|
||||
|
||||
$this->mockModel->method('getTranslatedEntity')
|
||||
->willReturn([$slot, $slot]);
|
||||
|
||||
$this->leadModel->method('getEntity')
|
||||
->with(123)
|
||||
->willReturn($contact);
|
||||
|
||||
$this->mockDispatcher->method('hasListeners')->willReturn(true);
|
||||
$matcher = $this->once();
|
||||
$this->mockDispatcher->expects($matcher)
|
||||
->method('dispatch')
|
||||
->willReturnCallback(
|
||||
function (...$parameters) use ($matcher, $contact, $slot) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (ContactFiltersEvaluateEvent $event) use ($contact, $slot) {
|
||||
$this->assertSame($contact, $event->getContact());
|
||||
$this->assertSame($slot->getFilters(), $event->getFilters());
|
||||
|
||||
// Match not found in any subscriber.
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE, $parameters[1]);
|
||||
}
|
||||
|
||||
return $parameters[0];
|
||||
}
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
'', // No content returned as the filter did not match anything.
|
||||
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetDynamicContentSlotForLeadWithNoListenerWithMatchingFilter(): void
|
||||
{
|
||||
$slotName = 'test';
|
||||
$contact = new Lead();
|
||||
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
|
||||
|
||||
$slot = new DynamicContent();
|
||||
$slot->setName($slotName);
|
||||
$slot->setIsCampaignBased(false);
|
||||
$slot->setFilters([['field' => 'email', 'type' => 'email', 'operator' => '=', 'filter' => 'ma@ka.t']]);
|
||||
$slot->setContent('<p>test</p>');
|
||||
|
||||
$this->mockModel->method('getEntities')
|
||||
->willReturn([$slot]);
|
||||
|
||||
$this->mockModel->method('getTranslatedEntity')
|
||||
->willReturn([$slot, $slot]);
|
||||
|
||||
$this->leadModel->method('getEntity')
|
||||
->with(123)
|
||||
->willReturn($contact);
|
||||
|
||||
$this->mockDispatcher->method('hasListeners')->willReturn(false);
|
||||
$matcher = $this->once();
|
||||
$this->mockDispatcher->expects($matcher)
|
||||
->method('dispatch')
|
||||
->willReturnCallback(
|
||||
function (...$parameters) use ($matcher, $contact, $slot) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$callback = function (TokenReplacementEvent $event) use ($contact, $slot) {
|
||||
$this->assertSame($contact, $event->getLead());
|
||||
$this->assertSame($slot->getContent(), $event->getContent());
|
||||
};
|
||||
$callback($parameters[0]);
|
||||
$this->assertSame(DynamicContentEvents::TOKEN_REPLACEMENT, $parameters[1]);
|
||||
}
|
||||
|
||||
return $parameters[0];
|
||||
}
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
'<p>test</p>',
|
||||
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetDynamicContentSlotForLeadWithNoListenerWithNotMatchingFilter(): void
|
||||
{
|
||||
$slotName = 'test';
|
||||
$contact = new Lead();
|
||||
$contact->setFields(['email' => 'ma@ka.t', 'id' => 123]);
|
||||
|
||||
$slot = new DynamicContent();
|
||||
$slot->setName($slotName);
|
||||
$slot->setIsCampaignBased(false);
|
||||
$slot->setFilters([['field' => 'email', 'type' => 'email', 'operator' => '=', 'filter' => 'uni@co.rn']]);
|
||||
$slot->setContent('<p>test</p>');
|
||||
|
||||
$this->mockModel->method('getEntities')
|
||||
->willReturn([$slot]);
|
||||
|
||||
$this->mockModel->method('getTranslatedEntity')
|
||||
->willReturn([$slot, $slot]);
|
||||
|
||||
$this->leadModel->method('getEntity')
|
||||
->with(123)
|
||||
->willReturn($contact);
|
||||
|
||||
$this->mockDispatcher->method('hasListeners')->willReturn(false);
|
||||
$this->mockDispatcher->expects($this->never())->method('dispatch');
|
||||
|
||||
Assert::assertSame(
|
||||
'',
|
||||
$this->helper->getDynamicContentSlotForLead($slotName, $contact)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Unit\Validator\Constraints;
|
||||
|
||||
use Mautic\DynamicContentBundle\Validator\Constraints\NoNesting;
|
||||
use Mautic\DynamicContentBundle\Validator\Constraints\NoNestingValidator;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||
use Symfony\Component\Translation\Translator;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Context\ExecutionContext;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class NoNestingValidatorTest extends TestCase
|
||||
{
|
||||
private const TRANSLATED_MESSAGE = 'DWC tokens cannot be used within another DWC.';
|
||||
private NoNesting $constraint;
|
||||
private NoNestingValidator $validator;
|
||||
private ExecutionContextInterface $context;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->constraint = new NoNesting();
|
||||
$this->validator = new NoNestingValidator();
|
||||
$this->context = $this->createContext();
|
||||
$this->context->setConstraint($this->constraint);
|
||||
$this->validator->initialize($this->context);
|
||||
}
|
||||
|
||||
public function testValidateWithInvalidConstraint(): void
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
$this->expectExceptionMessage(sprintf('Expected argument of type "%s"', NoNesting::class));
|
||||
$this->validator->validate('value', new NotBlank());
|
||||
}
|
||||
|
||||
public function testValidateWithInvalidType(): void
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
$this->expectExceptionMessage('Expected argument of type "string", "stdClass" given');
|
||||
$this->validator->validate(new \stdClass(), $this->constraint);
|
||||
}
|
||||
|
||||
public function testValidateWithNull(): void
|
||||
{
|
||||
$this->validator->validate(null, $this->constraint);
|
||||
Assert::assertCount(0, $this->context->getViolations(), 'No violation should be added for a null value.');
|
||||
}
|
||||
|
||||
public function testValidateWithValidValue(): void
|
||||
{
|
||||
$this->validator->validate('Some valid value', $this->constraint);
|
||||
Assert::assertCount(0, $this->context->getViolations(), 'No violation should be added for a valid value.');
|
||||
}
|
||||
|
||||
public function testValidateWithInvalidValue(): void
|
||||
{
|
||||
$this->validator->validate('Some invalid value {dwc=some}', $this->constraint);
|
||||
Assert::assertCount(1, $this->context->getViolations(), 'There should be one violation for an invalid value.');
|
||||
Assert::assertSame(self::TRANSLATED_MESSAGE, $this->context->getViolations()->get(0)->getMessage());
|
||||
}
|
||||
|
||||
private function createContext(): ExecutionContextInterface
|
||||
{
|
||||
$locale = 'en_US';
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
$translator = new Translator($locale);
|
||||
$translator->addLoader('array', new ArrayLoader());
|
||||
|
||||
$translator->addResource('array', [
|
||||
'mautic.dynamicContent.no_nesting' => self::TRANSLATED_MESSAGE,
|
||||
], $locale, 'validators');
|
||||
|
||||
return new ExecutionContext($validator, null, $translator, 'validators');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Tests\Validator\Constraints;
|
||||
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Mautic\DynamicContentBundle\Validator\Constraints\SlotNameType;
|
||||
use Mautic\DynamicContentBundle\Validator\Constraints\SlotNameTypeValidator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
class SlotNameTypeValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
/**
|
||||
* @var DynamicContentModel|MockObject
|
||||
*/
|
||||
private $dynamicContentModel;
|
||||
|
||||
protected function createValidator(): SlotNameTypeValidator
|
||||
{
|
||||
$this->dynamicContentModel = $this->createMock(DynamicContentModel::class);
|
||||
|
||||
return new SlotNameTypeValidator($this->dynamicContentModel);
|
||||
}
|
||||
|
||||
public function testValidSlotNameType(): void
|
||||
{
|
||||
$dynamicContent = new DynamicContent();
|
||||
$dynamicContent->setSlotName('slot1');
|
||||
$dynamicContent->setType('html');
|
||||
$dynamicContent->setIsCampaignBased(false);
|
||||
|
||||
$existingContent = new DynamicContent();
|
||||
$existingContent->setSlotName('slot1');
|
||||
$existingContent->setType('html');
|
||||
$dynamicContent->setIsCampaignBased(false);
|
||||
|
||||
$this->dynamicContentModel->method('checkEntityBySlotName')->willReturn(false);
|
||||
|
||||
$this->validator->validate($dynamicContent, new SlotNameType());
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testInvalidSlotNameType(): void
|
||||
{
|
||||
$dynamicContent = new DynamicContent();
|
||||
$dynamicContent->setSlotName('slot1');
|
||||
$dynamicContent->setType('text');
|
||||
$dynamicContent->setIsCampaignBased(false);
|
||||
|
||||
$existingContent = new DynamicContent();
|
||||
$existingContent->setSlotName('slot1');
|
||||
$existingContent->setType('html');
|
||||
$dynamicContent->setIsCampaignBased(false);
|
||||
|
||||
$this->dynamicContentModel->method('checkEntityBySlotName')->willReturn(true);
|
||||
|
||||
$constraint = new SlotNameType();
|
||||
$this->validator->validate($dynamicContent, $constraint);
|
||||
|
||||
$this->buildViolation($constraint->message)
|
||||
->atPath('property.path.type')
|
||||
->assertRaised();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mautic.dynamicContent.error.notfound="No Dynamic Content with an id of %id% was found!"
|
||||
mautic.dynamicContent.notice.batch_deleted="%count% entities have been deleted!"
|
||||
@@ -0,0 +1,115 @@
|
||||
mautic.dynamicContent.dynamicContent="Dynamic Web Content"
|
||||
mautic.dynamicContent.dynamicContents="Dynamic Web Content"
|
||||
|
||||
mautic.placeholder_tokens.dynamic_content_tokens="Dynamic Content"
|
||||
mautic.placeholder_tokens.dynamic_content.example="[Dynamic Content 1] | for example User-defined variable name"
|
||||
|
||||
mautic.dynamicContent.campaign.event.form.dynamicContents="Limit to Pages"
|
||||
mautic.dynamicContent.campaign.event.form.dynamicContents.descr="Select the pages this trigger applies to. If none are selected, it'll apply to any page."
|
||||
mautic.dynamicContent.campaign.event.form.url="URL"
|
||||
mautic.dynamicContent.campaign.event.form.url.descr="Insert the URL of the page where you placed the tracking pixel. It is possible to use wildcard (*). Example: *product/123* will match http://web.com/product/1234.htm. Separate multiple URLs with a comma."
|
||||
mautic.dynamicContent.campaign.event.dynamicContenthit="Visits a page"
|
||||
mautic.dynamicContent.campaign.event.dynamicContenthit_descr="Trigger actions on a page/url hit."
|
||||
|
||||
mautic.dynamicContent.config.form.cat.in.url="Show category in page URL?"
|
||||
mautic.dynamicContent.config.form.cat.in.url.tooltip="If enabled, the category slug will be included in the URL."
|
||||
mautic.dynamicContent.config.form.google.analytics="Analytics script (i.e. Google Analytics)"
|
||||
mautic.dynamicContent.config.form.google.analytics.tooltip="Insert the analytics script to have it automatically included in the source of landing pages."
|
||||
|
||||
mautic.dynamicContent.event.hit="Page Hit"
|
||||
mautic.dynamicContent.event.publish.down="Set %dwc% as unavailable"
|
||||
mautic.dynamicContent.event.publish.down.description="Dynamic Content '%dwc%' is going to become unavailable for use."
|
||||
mautic.dynamicContent.event.publish.up="Set %dwc% as unavailable"
|
||||
mautic.dynamicContent.event.publish.up.description="Page '%dwc%' is going to become available for use."
|
||||
|
||||
mautic.dynamiccontent.generator.default_content_tooltip="Add your own default content to ensure it displays when the filters aren't matching - for example with new anonymous visitors or a Contact that doesn't match the criteria you have specified."
|
||||
mautic.dynamiccontent.generator.deployment_options="Choose how to integrate your dynamic content"
|
||||
mautic.dynamiccontent.generator.deployment_instructions="Copy and paste the plugin code into your CMS editor, or add the HTML snippet directly to your website's code."
|
||||
mautic.dynamiccontent.generator.plugins="Plugins"
|
||||
mautic.dynamiccontent.generator.plugins_description="Compatible with popular CMS community plugins."
|
||||
mautic.dynamiccontent.generator.html="HTML"
|
||||
mautic.dynamiccontent.generator.html_description="Works with any website using our tracking script."
|
||||
mautic.dynamiccontent.generator.switch_code_wrapper="Switch code wrapper"
|
||||
mautic.dynamiccontent.generator.more_information.plugins="Information about code wrapper"
|
||||
mautic.dynamiccontent.generator.more_information.html="Information about HTML tags"
|
||||
mautic.dynamiccontent.generator.when_to_change_brackets="Different CMS plugins use different syntax"
|
||||
mautic.dynamiccontent.generator.joomla_syntax_info="Joomla uses curly braces {mautic}, while WordPress uses square brackets [mautic]. Choose the appropriate syntax based on your CMS plugin."
|
||||
mautic.dynamiccontent.generator.html_tag_title="Choosing the right HTML tag"
|
||||
mautic.dynamiccontent.generator.html_tag_info="Use div for block-level content that should start on a new line, or span for inline content that flows within your text. Both tags support the same dynamic content functionality."
|
||||
mautic.dynamiccontent.generator.preview_content_discard_notice="Any text entered in the input field will be lost when closing this window."
|
||||
mautic.dynamiccontent.generator.input_placeholder="Draft your default content here"
|
||||
mautic.dynamiccontent.generator.switch_html_tag="Switch HTML tag"
|
||||
mautic.dynamiccontent.generator.copy_dynamic_content_slot="Copy dynamic content slot"
|
||||
mautic.dynamiccontent.generator.troubleshooting.title="Troubleshooting"
|
||||
|
||||
mautic.dynamiccontent.generator.troubleshooting.content_not_displaying="Content not displaying"
|
||||
mautic.dynamiccontent.generator.troubleshooting.content_not_displaying_text="Try testing with an anonymous session or incognito window. Mautic ignores administrator activity."
|
||||
|
||||
mautic.dynamiccontent.generator.troubleshooting.javascript="JavaScript limitations"
|
||||
mautic.dynamiccontent.generator.troubleshooting.javascript_text="Mautic cleans your DWC for security, use elements with technical attributes in your content that can be targeted by external scripts instead."
|
||||
|
||||
mautic.dynamiccontent.generator.troubleshooting.default_content="Include default content"
|
||||
mautic.dynamiccontent.generator.troubleshooting.default_content_text="Ensure visitors always see something meaningful. This is crucial for anonymous visitors or when contacts don't match your specified criteria."
|
||||
|
||||
mautic.dynamiccontent.generator.troubleshooting.cache="Cache related issues"
|
||||
mautic.dynamiccontent.generator.troubleshooting.cache_text="Try disabling caching during testing, clearing your browser cache, and purging CDN cache if using services like Cloudflare."
|
||||
|
||||
mautic.dynamicContent.form.internal.name="Internal Name"
|
||||
mautic.dynamicContent.form.confirmbatchdelete="Delete the selected items? WARNING - this will also delete all associated translations!"
|
||||
mautic.dynamicContent.form.confirmdelete="Delete the dynamic content item, %name%? WARNING - this will also delete all associated translations!"
|
||||
mautic.dynamicContent.form.content="Content"
|
||||
mautic.dynamicContent.form.content.help="The content to return when this variant/default item is requested."
|
||||
mautic.dynamicContent.form.language.help="Translate the content into multiple languages and link them together by assigning them to a parent page. Language selection links will become available on the related landing pages for customers to choose the language they want to see."
|
||||
|
||||
mautic.dynamicContent.header.edit="Edit Dynamic Content - %name%"
|
||||
mautic.dynamicContent.header.new="New Dynamic Content"
|
||||
|
||||
mautic.dynamicContent.menu.edit="Edit Page"
|
||||
mautic.dynamicContent.menu.view="View Page"
|
||||
|
||||
mautic.dynamiccontent.permissions.header="Dynamic Content Permissions"
|
||||
mautic.dynamiccontent.permissions.dynamiccontents="Dynamic Content - User has access to"
|
||||
mautic.dynamicContent.publish.down="Set as unavailable"
|
||||
mautic.dynamicContent.publish.up="Set as available"
|
||||
mautic.dynamicContent.report.revision="Revision"
|
||||
|
||||
mautic.dynamicContent.campaign.send_dwc="Push dynamic content"
|
||||
mautic.dynamicContent.campaign.send_dwc.tooltip="Trigger actions when a Dynamic Content slot is requested."
|
||||
|
||||
mautic.dynamicContent.send.selectDynamicContents.default="Select Default Content"
|
||||
mautic.dynamicContent.send.selectDynamicContents="Select Dynamic Content"
|
||||
mautic.dynamicContent.send.new.dynamicContent="New Dynamic Content"
|
||||
mautic.dynamicContent.send.edit.dynamicContent="Edit Dynamic Content"
|
||||
mautic.dynamicContent.send.slot_name="Requested Slot Name"
|
||||
mautic.dynamicContent.label.slot_name="Slot Name"
|
||||
mautic.dynamicContent.send.slot_name.tooltip="The name of the slot the contact is requesting."
|
||||
mautic.dynamicContent.choose.dynamicContents="Select the default dynamic content to show."
|
||||
mautic.dynamicContent.campaign.decision_dwc="Request dynamic content"
|
||||
mautic.dynamicContent.campaign.decision_dwc.tooltip="This is the top level for a dynamic content request."
|
||||
|
||||
mautic.dynamicContent.error.notfound="Dynamic Content item %id% not found."
|
||||
mautic.dynamicContent.notice.batch_deleted="%count% dynamic content items deleted."
|
||||
mautic.dwc.form.confirmbatchdelete="Confirm batch delete"
|
||||
mautic.dwc.delete_new="Discard the new Dynamic Content"
|
||||
mautic.dwc.form.is.campaign="Campaign dynamic content"
|
||||
mautic.dwc.form.is.campaign.tooltip="Is campaign dynamic content"
|
||||
mautic.dwc.form.is.standalone="Standalone dynamic content"
|
||||
mautic.dwc.form.is_campaign_based="Is campaign based?"
|
||||
mautic.dwc.form.is_campaign_based.tooltip="Select whether the content will be displayed based on campaign conditions or filters"
|
||||
|
||||
mautic.campaign.dwc.decision="Request dynamic content"
|
||||
mautic.campaign.dwc.push_content="Push dynamic content"
|
||||
mautic.dynamic.content.triggered="Dynamic Content Triggered"
|
||||
mautic.dynamicContent.views="Views"
|
||||
mautic.dynamicContent.show.total.views="Total Views"
|
||||
mautic.dynamicContent.show.unique.views="Unique Views"
|
||||
mautic.dynamicContent.timeline.content="Content"
|
||||
mautic.dynamicContent.timeline.title="Read Dynamic Content"
|
||||
mautic.dynamicContent.token.slot="Dynamic Web Content Slot"
|
||||
mautic.dynamicContent.slot.campaign="Is campaign based"
|
||||
mautic.core.icon_tooltip.is_filter_based="Is a filter based dynamic web content"
|
||||
mautic.dynamicContent.header.is_filter_based="Is a filter based dynamic web content for slot: %slot%"
|
||||
|
||||
mautic.dynamicContent.type.label="Type"
|
||||
mautic.dynamic.content.type.html="HTML"
|
||||
mautic.dynamic.content.type.text="Text"
|
||||
@@ -0,0 +1,5 @@
|
||||
mautic.dynamicContent.name.notblank="Please enter a name."
|
||||
mautic.dynamicContent.slot_name.notblank="Please enter a slot name."
|
||||
mautic.dynamicContent.filter.options.empty="At least one filter is required."
|
||||
mautic.dynamicContent.no_nesting="DWC tokens cannot be used within another DWC. Please remove any DWC tokens from the content to proceed."
|
||||
mautic.dynamicContent.slot_name_type="All Dynamic Contents with the same slot name must be of the same type."
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* This constraint checks if the content does not include another DWC token.
|
||||
*/
|
||||
class NoNesting extends Constraint
|
||||
{
|
||||
public string $message = 'mautic.dynamicContent.no_nesting';
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Validator\Constraints;
|
||||
|
||||
use Mautic\DynamicContentBundle\Helper\DynamicContentHelper;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class NoNestingValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof NoNesting) {
|
||||
throw new UnexpectedTypeException($constraint, NoNesting::class);
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new UnexpectedTypeException($value, 'string');
|
||||
}
|
||||
|
||||
if (preg_match(DynamicContentHelper::DYNAMIC_WEB_CONTENT_REGEX, $value)) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* This constraint makes sure all entities with same slot name have the same type.
|
||||
*/
|
||||
class SlotNameType extends Constraint
|
||||
{
|
||||
public string $message = 'mautic.dynamicContent.slot_name_type';
|
||||
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\DynamicContentBundle\Validator\Constraints;
|
||||
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\DynamicContentBundle\Model\DynamicContentModel;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
class SlotNameTypeValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private DynamicContentModel $dynamicContentModel)
|
||||
{
|
||||
}
|
||||
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof SlotNameType) {
|
||||
throw new UnexpectedTypeException($constraint, SlotNameType::class);
|
||||
}
|
||||
|
||||
if (!$value instanceof DynamicContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$slotName = $value->getSlotName();
|
||||
if (empty($slotName) || $value->getIsCampaignBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingContents = $this->dynamicContentModel->
|
||||
checkEntityBySlotName($slotName, $value->getType(), '!=', $value->getId());
|
||||
if ($existingContents) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->atPath('type')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user