Feature: embedded layout mode and schedule improvements

- Add layout-embedded class for nested entity selectors
- Simplified styling for embedded widgets (less padding, borders)
- Schedule toggle row with summary badges
- Summary badges show datetime range, weekly schedule, holiday count
- Flag fallback styling for countries without valid ISO codes
- Section hint margin after embedded entity selector
- Holiday countries group without modifiers section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 10:52:13 +01:00
parent 7d4d1ec618
commit 4eeb8d85ae
12 changed files with 12458 additions and 49 deletions

View File

@@ -68,13 +68,31 @@
return;
}
var html = '<span class="entity-chip" data-id="' + this.escapeAttr(id) + '">';
// Check if this is a country entity (for flag and holiday preview)
var blockType = $block.data('blockType') || '';
var searchEntity = $picker.attr('data-search-entity') || blockType;
var isCountry = (searchEntity === 'countries');
if (data && data.image) {
var html = '<span class="entity-chip" data-id="' + this.escapeAttr(id) + '"';
if (isCountry && data && data.iso_code) {
html += ' data-iso="' + this.escapeAttr(data.iso_code) + '"';
}
html += '>';
// Country: show flag
if (isCountry && data && data.iso_code) {
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + this.escapeAttr(data.iso_code.toLowerCase()) + '.png" alt="' + this.escapeAttr(data.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><i class="icon-flag flag-fallback" style="display:none;"></i></span>';
} else if (data && data.image) {
html += '<span class="chip-icon"><img src="' + this.escapeAttr(data.image) + '" alt=""></span>';
}
html += '<span class="chip-name">' + this.escapeHtml(name) + '</span>';
// Country: add holiday preview button
if (isCountry) {
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
}
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '</span>';
@@ -343,11 +361,14 @@
loadExistingSelections: function() {
var self = this;
console.log('[EntitySelector] loadExistingSelections called for id:', this.config.id);
// Collect all entity IDs to load, grouped by entity type
var entitiesToLoad = {}; // { entity_type: { ids: [], pickers: [] } }
console.log('[EntitySelector] Looking for .selection-group in wrapper:', this.$wrapper.length ? 'found' : 'NOT FOUND');
this.$wrapper.find('.selection-group').each(function() {
console.log('[EntitySelector] Found .selection-group, index:', $(this).data('groupIndex'));
var $group = $(this);
var $block = $group.closest('.target-block');
var blockType = $block.data('blockType');
@@ -394,9 +415,12 @@
// Skip AJAX if no entities to load
if (!hasEntities) {
console.log('[EntitySelector] No entities to load, skipping AJAX');
return;
}
console.log('[EntitySelector] Making bulk AJAX request for entities:', JSON.stringify(bulkRequest));
// Single bulk AJAX call for all entity types
$.ajax({
url: self.config.ajaxUrl,
@@ -409,9 +433,12 @@
entities: JSON.stringify(bulkRequest)
},
success: function(response) {
console.log('[EntitySelector] AJAX response:', response);
if (!response.success || !response.entities) {
console.log('[EntitySelector] Response failed or no entities');
return;
}
try {
// Process each entity type's results
Object.keys(entitiesToLoad).forEach(function(entityType) {
@@ -431,6 +458,9 @@
var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
var validIds = [];
// Check if this is a country entity
var isCountry = (entityType === 'countries');
// Replace loading chips with real data
pickerData.ids.forEach(function(id) {
var $loadingChip = $chips.find('.entity-chip-loading[data-id="' + id + '"]');
@@ -439,11 +469,26 @@
validIds.push(entity.id);
// Create real chip
var html = '<span class="entity-chip" data-id="' + self.escapeAttr(entity.id) + '">';
if (entity.image) {
var html = '<span class="entity-chip" data-id="' + self.escapeAttr(entity.id) + '"';
if (isCountry && entity.iso_code) {
html += ' data-iso="' + self.escapeAttr(entity.iso_code) + '"';
}
html += '>';
// Country: show flag
if (isCountry && entity.iso_code) {
html += '<span class="chip-flag"><img src="https://flagcdn.com/16x12/' + self.escapeAttr(entity.iso_code.toLowerCase()) + '.png" alt="' + self.escapeAttr(entity.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><i class="icon-flag flag-fallback" style="display:none;"></i></span>';
} else if (entity.image) {
html += '<span class="chip-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
}
html += '<span class="chip-name">' + self.escapeHtml(entity.name) + '</span>';
// Country: add holiday preview button
if (isCountry) {
html += '<button type="button" class="chip-preview-holidays" title="Preview holidays"><i class="material-icons">visibility</i></button>';
}
html += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '</span>';
@@ -466,6 +511,16 @@
self.updateBlockStatus($picker.closest('.target-block'));
});
});
// Update condition counts after chips are loaded (for holiday counts, etc.)
self.updateAllConditionCounts();
} catch (e) {
console.error('[EntitySelector] Error processing AJAX response:', e);
}
},
error: function(xhr, status, error) {
console.error('[EntitySelector] AJAX request failed:', status, error, xhr.responseText);
}
});
},
@@ -475,23 +530,30 @@
* Also shows loading placeholders for entity_search types
*/
collectPickerEntities: function($picker, blockType, entitiesToLoad) {
console.log('[EntitySelector] collectPickerEntities called, blockType:', blockType, 'picker length:', $picker.length);
if (!$picker.length) {
console.log('[EntitySelector] Picker not found, returning');
return;
}
var self = this;
var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
console.log('[EntitySelector] Looking for values-data input, found:', $dataInput.length);
if (!$dataInput.length) {
console.log('[EntitySelector] No data input found, returning');
return;
}
var valueType = $picker.attr('data-value-type');
var rawValue = $dataInput.val() || '[]';
console.log('[EntitySelector] valueType:', valueType, 'rawValue:', rawValue);
var values = [];
try {
values = JSON.parse(rawValue);
console.log('[EntitySelector] Parsed values:', values);
} catch (e) {
console.log('[EntitySelector] JSON parse error:', e);
return;
}

View File

@@ -548,11 +548,15 @@
var self = this;
var data = {};
console.log('[EntitySelector] serializeAllBlocks called');
this.$wrapper.find('.target-block').each(function() {
var $block = $(this);
var blockType = $block.data('blockType');
var groups = self.getBlockGroups($block);
console.log('[EntitySelector] Block:', blockType, 'Groups:', groups.length);
// Groups now contain their own modifiers, no block-level modifiers
if (groups.length > 0) {
data[blockType] = { groups: groups };
@@ -563,7 +567,13 @@
// Update hidden input first
var $input = this.$wrapper.find('input[name="' + this.config.name + '"]');
$input.val(JSON.stringify(data));
var jsonData = JSON.stringify(data);
console.log('[EntitySelector] Hidden input name:', this.config.name);
console.log('[EntitySelector] Hidden input found:', $input.length);
console.log('[EntitySelector] Serialized data:', jsonData.substring(0, 500));
$input.val(jsonData);
// Then update tab badges (reads from hidden input)
this.updateTabBadges();
@@ -901,7 +911,9 @@
* Extract condition data from a row for bulk counting
*/
getConditionData: function($row, blockType) {
console.log('[getConditionData] Called with blockType:', blockType);
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
console.log('[getConditionData] $countEl found:', $countEl.length);
if (!$countEl.length) return null;
var isExclude = $row.hasClass('exclude-row');
@@ -910,6 +922,7 @@
: $row.find('.include-method-select');
var method = $methodSelect.val();
console.log('[getConditionData] method:', method);
if (!method) {
$countEl.hide();
return null;
@@ -918,18 +931,49 @@
var $picker = isExclude
? $row.find('.exclude-picker')
: $row.find('.include-picker');
console.log('[getConditionData] $picker found:', $picker.length, 'data-value-type attr:', $picker.attr('data-value-type'));
var valueType = $picker.data('valueType') || $picker.attr('data-value-type') || 'none';
console.log('[getConditionData] valueType:', valueType);
// Special case: "All countries" method - needs separate handling for holidays
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
console.log('[getConditionData] All countries detected - triggering updateConditionCount');
// Trigger separate update for this special case (uses nested AJAX)
var self = this;
setTimeout(function() {
self.updateConditionCount($row, blockType);
}, 0);
return null; // Skip bulk processing, handled separately
}
// Special case: Specific countries with entity_search - needs holiday counting, not entity counting
var searchEntity = $picker.attr('data-search-entity') || '';
if (blockType === 'countries' && valueType === 'entity_search' && searchEntity === 'countries') {
console.log('[getConditionData] Specific countries detected - triggering updateConditionCount for holiday counting');
var self = this;
setTimeout(function() {
self.updateConditionCount($row, blockType);
}, 0);
return null; // Skip bulk processing, handled separately
}
// Hide badge for other "all" type methods (valueType === 'none') since they don't filter
if (valueType === 'none') {
$countEl.hide();
return null;
}
var valueType = $picker.data('valueType') || 'none';
var values = this.getPickerValues($picker, valueType);
// Don't count if no values (except for boolean/all methods)
// Don't count if no values (except for boolean methods)
var hasNoValues = !values ||
(Array.isArray(values) && values.length === 0) ||
(typeof values === 'object' && !Array.isArray(values) && (
(valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
));
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) {
if (valueType !== 'boolean' && hasNoValues) {
$countEl.hide();
return null;
}
@@ -983,7 +1027,10 @@
var self = this;
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first();
if (!$countEl.length) return;
if (!$countEl.length) {
console.log('[updateConditionCount] No $countEl found');
return;
}
var isExclude = $row.hasClass('exclude-row');
var $methodSelect = isExclude
@@ -991,7 +1038,9 @@
: $row.find('.include-method-select');
var method = $methodSelect.val();
console.log('[updateConditionCount] method:', method, 'isExclude:', isExclude);
if (!method) {
console.log('[updateConditionCount] No method, hiding badge');
$countEl.hide();
return;
}
@@ -1001,6 +1050,104 @@
: $row.find('.include-picker');
var valueType = $picker.data('valueType') || 'none';
var searchEntity = $picker.attr('data-search-entity') || '';
// Get the block type to check if this is a countries block
if (!blockType) {
var $block = $row.closest('.target-block');
blockType = $block.data('blockType') || 'products';
}
console.log('[updateConditionCount] valueType:', valueType, 'searchEntity:', searchEntity, 'blockType:', blockType, 'method:', method);
// Special case: "All countries" method - fetch holidays for all countries
if (valueType === 'none' && blockType === 'countries' && method === 'all') {
console.log('[updateConditionCount] All countries method - fetching all country holidays');
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.removeClass('clickable no-matches country-holidays').show();
// First fetch all active country IDs, then get holidays
$.ajax({
url: self.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'searchTargetEntities',
trait: 'EntitySelector',
entity_type: 'countries',
query: '',
limit: 500
},
success: function(response) {
var items = response.results || response.items || [];
if (response && response.success && items.length > 0) {
var allCountryIds = items.map(function(item) { return item.id; });
console.log('[updateConditionCount] Found', allCountryIds.length, 'countries, fetching holidays');
// Store condition data for click handler
$countEl.data('conditionData', {
method: method,
values: allCountryIds,
blockType: blockType,
isExclude: isExclude,
isCountryHolidays: true,
countryIds: allCountryIds,
isAllCountries: true
});
// Now fetch holiday count
$.ajax({
url: self.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getHolidaysForCountries',
trait: 'EntitySelector',
country_ids: allCountryIds.join(','),
count_only: 1
},
success: function(holidayResponse) {
console.log('[updateConditionCount] All countries holiday response:', holidayResponse);
if (holidayResponse && holidayResponse.success) {
var count = holidayResponse.total_count || 0;
$countEl.removeClass('no-matches clickable');
$countEl.addClass('country-holidays');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
}
$countEl.data('countriesInfo', holidayResponse.countries || []);
} else {
$countEl.hide().removeClass('clickable');
}
},
error: function() {
$countEl.hide().removeClass('clickable');
}
});
} else {
$countEl.hide().removeClass('clickable');
}
},
error: function() {
$countEl.hide().removeClass('clickable');
}
});
return;
}
// Hide badge for other "all" type methods (valueType === 'none') since they don't filter
if (valueType === 'none') {
console.log('[updateConditionCount] valueType is none, hiding badge');
$countEl.hide();
return;
}
var values = this.getPickerValues($picker, valueType);
var hasNoValues = !values ||
@@ -1009,7 +1156,7 @@
(valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0)
));
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) {
if (valueType !== 'boolean' && hasNoValues) {
$countEl.hide();
return;
}
@@ -1019,9 +1166,65 @@
blockType = $block.data('blockType') || 'products';
}
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.removeClass('clickable no-matches').show();
// Check if this is a country selection - show holiday count instead
var isCountrySelection = (searchEntity === 'countries' && valueType === 'entity_search');
console.log('[updateConditionCount] isCountrySelection:', isCountrySelection, 'values:', values);
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>');
$countEl.removeClass('clickable no-matches country-holidays').show();
// For countries, fetch holiday count
if (isCountrySelection && Array.isArray(values) && values.length > 0) {
console.log('[updateConditionCount] Fetching holiday count for countries:', values);
$countEl.data('conditionData', {
method: method,
values: values,
blockType: blockType,
isExclude: isExclude,
isCountryHolidays: true,
countryIds: values
});
$.ajax({
url: self.config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'getHolidaysForCountries',
trait: 'EntitySelector',
country_ids: values.join(','),
count_only: 1
},
success: function(response) {
console.log('[updateConditionCount] Holiday response:', response);
if (response && response.success) {
var count = response.total_count || 0;
console.log('[updateConditionCount] Holiday count:', count);
$countEl.removeClass('no-matches clickable');
$countEl.addClass('country-holidays');
if (count === 0) {
$countEl.find('.preview-count').text(count);
$countEl.addClass('no-matches').show();
} else {
$countEl.find('.preview-count').text(count);
$countEl.addClass('clickable').show();
}
// Store countries info for popover
$countEl.data('countriesInfo', response.countries || []);
} else {
console.log('[updateConditionCount] Holiday response failed:', response);
$countEl.hide().removeClass('clickable');
}
},
error: function() {
$countEl.hide().removeClass('clickable');
}
});
return;
}
// Default: count entities
$countEl.data('conditionData', {
method: method,
values: values,

View File

@@ -233,6 +233,22 @@
requestData.filter_active = 1;
}
}
// Countries-specific
if (searchEntity === 'countries') {
if (this.filters.activeOnly) {
requestData.filter_active = 1;
}
if (this.filters.hasHolidays) {
requestData.filter_has_holidays = 1;
}
if (this.filters.containsStates) {
requestData.filter_contains_states = 1;
}
if (this.filters.zone) {
requestData.filter_zone = this.filters.zone;
}
}
}
$.ajax({
@@ -357,16 +373,22 @@
html += 'data-name="' + self.escapeAttr(item.name) + '"';
if (item.image) html += ' data-image="' + self.escapeAttr(item.image) + '"';
if (item.subtitle) html += ' data-subtitle="' + self.escapeAttr(item.subtitle) + '"';
if (item.iso_code) html += ' data-iso="' + self.escapeAttr(item.iso_code) + '"';
html += '>';
html += '<span class="result-checkbox"><i class="icon-check"></i></span>';
if (item.image) {
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : null;
// Countries show flags
if (searchEntity === 'countries' && item.iso_code) {
var flagUrl = 'https://flagcdn.com/w40/' + item.iso_code.toLowerCase() + '.png';
html += '<div class="result-image result-flag"><img src="' + self.escapeAttr(flagUrl) + '" alt="' + self.escapeAttr(item.iso_code) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';"><span class="flag-fallback" style="display:none;"><i class="icon-flag"></i></span></div>';
} else if (item.image) {
html += '<div class="result-image"><img src="' + self.escapeAttr(item.image) + '" alt=""></div>';
} else {
// Entity-specific icons
var iconClass = 'icon-cube'; // default
var searchEntity = self.activeGroup ? self.activeGroup.searchEntity : null;
if (searchEntity === 'categories') iconClass = 'icon-folder';
else if (searchEntity === 'manufacturers') iconClass = 'icon-building';
else if (searchEntity === 'suppliers') iconClass = 'icon-truck';
@@ -628,7 +650,11 @@
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
};
if (this.$dropdown) {
@@ -657,6 +683,10 @@
this.$dropdown.find('.filter-has-image').prop('checked', false);
this.$dropdown.find('.filter-active-only').prop('checked', true);
this.$dropdown.find('.filter-attribute-group-select, .filter-feature-group-select').val('');
// Country filters
this.$dropdown.find('.filter-has-holidays').prop('checked', false);
this.$dropdown.find('.filter-contains-states').prop('checked', false);
this.$dropdown.find('.filter-zone-select').val('');
}
this.refreshSearch();
@@ -689,7 +719,11 @@
dateAddFrom: null,
dateAddTo: null,
lastProductFrom: null,
lastProductTo: null
lastProductTo: null,
// Country-specific filters
hasHolidays: false,
containsStates: false,
zone: null
};
if (this.$dropdown) {
@@ -716,6 +750,10 @@
this.$dropdown.find('.filter-has-image').prop('checked', false);
this.$dropdown.find('.filter-active-only').prop('checked', true);
this.$dropdown.find('.filter-attribute-group-select, .filter-feature-group-select').val('');
// Country filters
this.$dropdown.find('.filter-has-holidays').prop('checked', false);
this.$dropdown.find('.filter-contains-states').prop('checked', false);
this.$dropdown.find('.filter-zone-select').val('');
}
// Note: Does NOT call refreshSearch() - caller handles search/load
},
@@ -781,6 +819,9 @@
$panel.find('.filter-row-entity-cms').show();
} else if (entityType === 'cms_categories') {
$panel.find('.filter-row-entity-cms-categories').show();
} else if (entityType === 'countries') {
$panel.find('.filter-row-entity-countries').show();
this.loadZonesForCountryFilter();
}
},