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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -68,13 +68,31 @@
return; 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-icon"><img src="' + this.escapeAttr(data.image) + '" alt=""></span>';
} }
html += '<span class="chip-name">' + this.escapeHtml(name) + '</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 += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '</span>'; html += '</span>';
@@ -343,11 +361,14 @@
loadExistingSelections: function() { loadExistingSelections: function() {
var self = this; var self = this;
console.log('[EntitySelector] loadExistingSelections called for id:', this.config.id);
// Collect all entity IDs to load, grouped by entity type // Collect all entity IDs to load, grouped by entity type
var entitiesToLoad = {}; // { entity_type: { ids: [], pickers: [] } } 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() { this.$wrapper.find('.selection-group').each(function() {
console.log('[EntitySelector] Found .selection-group, index:', $(this).data('groupIndex'));
var $group = $(this); var $group = $(this);
var $block = $group.closest('.target-block'); var $block = $group.closest('.target-block');
var blockType = $block.data('blockType'); var blockType = $block.data('blockType');
@@ -394,9 +415,12 @@
// Skip AJAX if no entities to load // Skip AJAX if no entities to load
if (!hasEntities) { if (!hasEntities) {
console.log('[EntitySelector] No entities to load, skipping AJAX');
return; return;
} }
console.log('[EntitySelector] Making bulk AJAX request for entities:', JSON.stringify(bulkRequest));
// Single bulk AJAX call for all entity types // Single bulk AJAX call for all entity types
$.ajax({ $.ajax({
url: self.config.ajaxUrl, url: self.config.ajaxUrl,
@@ -409,9 +433,12 @@
entities: JSON.stringify(bulkRequest) entities: JSON.stringify(bulkRequest)
}, },
success: function(response) { success: function(response) {
console.log('[EntitySelector] AJAX response:', response);
if (!response.success || !response.entities) { if (!response.success || !response.entities) {
console.log('[EntitySelector] Response failed or no entities');
return; return;
} }
try {
// Process each entity type's results // Process each entity type's results
Object.keys(entitiesToLoad).forEach(function(entityType) { Object.keys(entitiesToLoad).forEach(function(entityType) {
@@ -431,6 +458,9 @@
var $dataInput = $picker.find('.include-values-data, .exclude-values-data'); var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
var validIds = []; var validIds = [];
// Check if this is a country entity
var isCountry = (entityType === 'countries');
// Replace loading chips with real data // Replace loading chips with real data
pickerData.ids.forEach(function(id) { pickerData.ids.forEach(function(id) {
var $loadingChip = $chips.find('.entity-chip-loading[data-id="' + id + '"]'); var $loadingChip = $chips.find('.entity-chip-loading[data-id="' + id + '"]');
@@ -439,11 +469,26 @@
validIds.push(entity.id); validIds.push(entity.id);
// Create real chip // Create real chip
var html = '<span class="entity-chip" data-id="' + self.escapeAttr(entity.id) + '">'; var html = '<span class="entity-chip" data-id="' + self.escapeAttr(entity.id) + '"';
if (entity.image) { 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-icon"><img src="' + self.escapeAttr(entity.image) + '" alt=""></span>';
} }
html += '<span class="chip-name">' + self.escapeHtml(entity.name) + '</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 += '<button type="button" class="chip-remove" title="Remove"><i class="icon-times"></i></button>';
html += '</span>'; html += '</span>';
@@ -466,6 +511,16 @@
self.updateBlockStatus($picker.closest('.target-block')); 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 * Also shows loading placeholders for entity_search types
*/ */
collectPickerEntities: function($picker, blockType, entitiesToLoad) { collectPickerEntities: function($picker, blockType, entitiesToLoad) {
console.log('[EntitySelector] collectPickerEntities called, blockType:', blockType, 'picker length:', $picker.length);
if (!$picker.length) { if (!$picker.length) {
console.log('[EntitySelector] Picker not found, returning');
return; return;
} }
var self = this; var self = this;
var $dataInput = $picker.find('.include-values-data, .exclude-values-data'); var $dataInput = $picker.find('.include-values-data, .exclude-values-data');
console.log('[EntitySelector] Looking for values-data input, found:', $dataInput.length);
if (!$dataInput.length) { if (!$dataInput.length) {
console.log('[EntitySelector] No data input found, returning');
return; return;
} }
var valueType = $picker.attr('data-value-type'); var valueType = $picker.attr('data-value-type');
var rawValue = $dataInput.val() || '[]'; var rawValue = $dataInput.val() || '[]';
console.log('[EntitySelector] valueType:', valueType, 'rawValue:', rawValue);
var values = []; var values = [];
try { try {
values = JSON.parse(rawValue); values = JSON.parse(rawValue);
console.log('[EntitySelector] Parsed values:', values);
} catch (e) { } catch (e) {
console.log('[EntitySelector] JSON parse error:', e);
return; return;
} }

View File

@@ -548,11 +548,15 @@
var self = this; var self = this;
var data = {}; var data = {};
console.log('[EntitySelector] serializeAllBlocks called');
this.$wrapper.find('.target-block').each(function() { this.$wrapper.find('.target-block').each(function() {
var $block = $(this); var $block = $(this);
var blockType = $block.data('blockType'); var blockType = $block.data('blockType');
var groups = self.getBlockGroups($block); var groups = self.getBlockGroups($block);
console.log('[EntitySelector] Block:', blockType, 'Groups:', groups.length);
// Groups now contain their own modifiers, no block-level modifiers // Groups now contain their own modifiers, no block-level modifiers
if (groups.length > 0) { if (groups.length > 0) {
data[blockType] = { groups: groups }; data[blockType] = { groups: groups };
@@ -563,7 +567,13 @@
// Update hidden input first // Update hidden input first
var $input = this.$wrapper.find('input[name="' + this.config.name + '"]'); 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) // Then update tab badges (reads from hidden input)
this.updateTabBadges(); this.updateTabBadges();
@@ -901,7 +911,9 @@
* Extract condition data from a row for bulk counting * Extract condition data from a row for bulk counting
*/ */
getConditionData: function($row, blockType) { 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(); 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; if (!$countEl.length) return null;
var isExclude = $row.hasClass('exclude-row'); var isExclude = $row.hasClass('exclude-row');
@@ -910,6 +922,7 @@
: $row.find('.include-method-select'); : $row.find('.include-method-select');
var method = $methodSelect.val(); var method = $methodSelect.val();
console.log('[getConditionData] method:', method);
if (!method) { if (!method) {
$countEl.hide(); $countEl.hide();
return null; return null;
@@ -918,18 +931,49 @@
var $picker = isExclude var $picker = isExclude
? $row.find('.exclude-picker') ? $row.find('.exclude-picker')
: $row.find('.include-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); 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 || var hasNoValues = !values ||
(Array.isArray(values) && values.length === 0) || (Array.isArray(values) && values.length === 0) ||
(typeof values === 'object' && !Array.isArray(values) && ( (typeof values === 'object' && !Array.isArray(values) && (
(valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) || (valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0) (valueType !== 'combination_attributes' && Object.keys(values).length === 0)
)); ));
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) { if (valueType !== 'boolean' && hasNoValues) {
$countEl.hide(); $countEl.hide();
return null; return null;
} }
@@ -983,7 +1027,10 @@
var self = this; var self = this;
var $countEl = $row.find('.method-selector-wrapper > .condition-match-count, > .exclude-header-row .condition-match-count').first(); 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 isExclude = $row.hasClass('exclude-row');
var $methodSelect = isExclude var $methodSelect = isExclude
@@ -991,7 +1038,9 @@
: $row.find('.include-method-select'); : $row.find('.include-method-select');
var method = $methodSelect.val(); var method = $methodSelect.val();
console.log('[updateConditionCount] method:', method, 'isExclude:', isExclude);
if (!method) { if (!method) {
console.log('[updateConditionCount] No method, hiding badge');
$countEl.hide(); $countEl.hide();
return; return;
} }
@@ -1001,6 +1050,104 @@
: $row.find('.include-picker'); : $row.find('.include-picker');
var valueType = $picker.data('valueType') || 'none'; 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 values = this.getPickerValues($picker, valueType);
var hasNoValues = !values || var hasNoValues = !values ||
@@ -1009,7 +1156,7 @@
(valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) || (valueType === 'combination_attributes' && values.attributes !== undefined && Object.keys(values.attributes).length === 0) ||
(valueType !== 'combination_attributes' && Object.keys(values).length === 0) (valueType !== 'combination_attributes' && Object.keys(values).length === 0)
)); ));
if (valueType !== 'none' && valueType !== 'boolean' && hasNoValues) { if (valueType !== 'boolean' && hasNoValues) {
$countEl.hide(); $countEl.hide();
return; return;
} }
@@ -1019,9 +1166,65 @@
blockType = $block.data('blockType') || 'products'; blockType = $block.data('blockType') || 'products';
} }
$countEl.find('.preview-count').html('<i class="icon-spinner icon-spin"></i>'); // Check if this is a country selection - show holiday count instead
$countEl.removeClass('clickable no-matches').show(); 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', { $countEl.data('conditionData', {
method: method, method: method,
values: values, values: values,

View File

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

View File

@@ -3,6 +3,7 @@
* Entity chips, selection pills, tags * Entity chips, selection pills, tags
*/ */
@use "sass:color";
@use '../variables' as *; @use '../variables' as *;
@use '../mixins' as *; @use '../mixins' as *;
@@ -285,6 +286,57 @@
} }
} }
// Country flag in chip
.chip-flag {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
img {
width: 18px;
height: 12px;
object-fit: cover;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}
.flag-fallback {
width: 18px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e8eaed 0%, #dadce0 100%);
border-radius: 2px;
font-size: 10px;
color: #5f6368;
}
}
// Holiday preview button in country chip
.chip-preview-holidays {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: $es-primary;
border-radius: 50%;
flex-shrink: 0;
transition: all $es-transition-fast;
&:hover {
background: rgba($es-primary, 0.15);
color: darken($es-primary, 10%);
}
i.material-icons {
font-size: 14px;
}
}
.chip-text, .chip-text,
.chip-name { .chip-name {
// Show full name, no truncation // Show full name, no truncation
@@ -344,7 +396,7 @@
.entity-chip.chip-warning { .entity-chip.chip-warning {
background: $es-warning-light; background: $es-warning-light;
color: darken($es-warning, 20%); color: color.adjust($es-warning, $lightness: -20%);
&:hover { &:hover {
background: rgba($es-warning, 0.3); background: rgba($es-warning, 0.3);
@@ -715,6 +767,238 @@
} }
} }
// ==========================================================================
// Holiday Preview Popover (Country chip eye button)
// ==========================================================================
.holiday-preview-popover {
position: absolute;
z-index: 10001;
width: 320px;
max-width: 90vw;
background: $es-white;
border-radius: $es-radius-lg;
box-shadow: $es-shadow-xl;
overflow: hidden;
.popover-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: $es-spacing-sm;
padding: $es-spacing-sm $es-spacing-md;
background: $es-bg-header;
border-bottom: 1px solid $es-border-color;
}
.popover-title {
display: flex;
align-items: center;
gap: $es-spacing-sm;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.popover-flag {
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}
.popover-close {
@include button-reset;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: $es-text-muted;
border-radius: $es-radius-md;
transition: all $es-transition-fast;
&:hover {
background: $es-slate-200;
color: $es-text-secondary;
}
i.material-icons {
font-size: 18px;
}
}
.popover-body {
max-height: 350px;
overflow-y: auto;
padding: $es-spacing-sm;
@include custom-scrollbar;
}
// Loading state
.holiday-preview-loading {
display: flex;
align-items: center;
justify-content: center;
gap: $es-spacing-sm;
padding: $es-spacing-xl 0;
color: $es-text-muted;
font-size: $es-font-size-sm;
i.material-icons {
font-size: 20px;
}
.icon-spin {
animation: spin 1s linear infinite;
}
}
// Empty state
.holiday-preview-empty {
text-align: center;
padding: $es-spacing-xl 0;
color: $es-text-muted;
i.material-icons {
font-size: 48px;
opacity: 0.4;
margin-bottom: $es-spacing-sm;
display: block;
}
p {
margin: 0;
font-size: $es-font-size-sm;
}
}
// Holiday list
.holiday-list {
display: flex;
flex-direction: column;
gap: $es-spacing-xs;
}
.holiday-item {
display: flex;
align-items: flex-start;
gap: $es-spacing-md;
padding: $es-spacing-sm $es-spacing-md;
background: $es-slate-50;
border-radius: $es-radius-md;
border-left: 3px solid $es-success;
&.holiday-type-bank,
&.holiday-type-bank-holiday {
border-left-color: $es-info;
}
&.holiday-type-observance {
border-left-color: $es-warning;
}
&.holiday-type-regional,
&.holiday-type-local-holiday {
border-left-color: #8b5cf6;
}
}
.holiday-date {
flex-shrink: 0;
min-width: 80px;
.holiday-day {
display: block;
font-size: $es-font-size-sm;
font-weight: $es-font-weight-semibold;
color: $es-text-primary;
}
.holiday-weekday {
display: block;
font-size: $es-font-size-xs;
color: $es-text-muted;
}
}
.holiday-info {
flex: 1;
min-width: 0;
}
.holiday-country-flag {
vertical-align: middle;
margin-right: 0.25rem;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}
.holiday-name {
display: inline;
font-size: $es-font-size-sm;
color: $es-text-primary;
word-wrap: break-word;
}
.holiday-type-badge {
display: inline-block;
margin-left: $es-spacing-sm;
padding: 0.125rem 0.375rem;
font-size: 10px;
font-weight: $es-font-weight-medium;
text-transform: capitalize;
background: $es-slate-200;
color: $es-text-secondary;
border-radius: $es-radius-sm;
vertical-align: middle;
}
.holiday-preview-note {
margin-top: $es-spacing-md;
font-size: $es-font-size-xs;
color: $es-text-muted;
text-align: center;
}
// Filter input
.popover-filter {
display: flex;
align-items: center;
gap: $es-spacing-xs;
padding: $es-spacing-xs $es-spacing-md;
border-bottom: 1px solid $es-border-color;
background: $es-slate-50;
i.material-icons {
font-size: 18px;
color: $es-text-muted;
}
.holiday-filter-input {
flex: 1;
border: none;
background: transparent;
font-size: $es-font-size-sm;
color: $es-text-primary;
outline: none;
padding: $es-spacing-xs 0;
&::placeholder {
color: $es-text-muted;
}
}
}
}
// Spin animation for loading icons
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// Bootstrap specificity overrides for chips toolbar form elements // Bootstrap specificity overrides for chips toolbar form elements
// PrestaShop admin uses #content .mpr-config-form... with high specificity // PrestaShop admin uses #content .mpr-config-form... with high specificity
// We need to match or exceed that specificity // We need to match or exceed that specificity

View File

@@ -208,6 +208,29 @@
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
&.result-flag {
width: 32px;
height: 24px;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
background: transparent;
img {
object-fit: contain;
}
.flag-fallback {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #e8eaed 0%, #dadce0 100%);
font-size: 14px;
color: #5f6368;
}
}
} }
.result-icon { .result-icon {

View File

@@ -306,3 +306,64 @@
border-radius: $es-radius-lg; border-radius: $es-radius-lg;
} }
} }
// Schedule toggle row (form-content layout)
.schedule-toggle-row {
display: flex;
align-items: center;
background: $es-slate-100;
border: 1px solid $es-border-color;
border-radius: $es-radius-lg;
.schedule-toggle-switch {
padding: $es-spacing-sm $es-spacing-md;
}
.schedule-toggle-actions {
padding: $es-spacing-sm $es-spacing-md;
border-left: 1px solid $es-border-color;
cursor: pointer;
transition: background-color $es-transition-fast;
&:hover {
background: $es-slate-200;
}
.material-icons {
color: $es-slate-400;
font-size: 20px;
}
}
}
// Schedule summary badges (read-only indicators in header)
.schedule-summary-badges {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
padding: 0 $es-spacing-sm;
}
.schedule-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: $es-slate-200;
color: $es-slate-600;
font-size: $es-font-size-xs;
font-weight: $es-font-weight-medium;
border-radius: $es-radius-full;
white-space: nowrap;
.material-icons {
font-size: 14px;
opacity: 0.7;
}
}
// Section hint after embedded entity selector - add margin
.schedule-holidays .section-hint {
margin-top: $es-spacing-md;
}

View File

@@ -20,9 +20,10 @@
} }
// Full-width form group override using :has() // Full-width form group override using :has()
.form-group:has(.entity-selector-trait), // Excludes .layout-form-group which uses standard PrestaShop form layout
.form-group:has(.target-conditions-trait), .form-group:has(.entity-selector-trait:not(.layout-form-group)),
.form-group:has(.condition-trait) { .form-group:has(.target-conditions-trait:not(.layout-form-group)),
.form-group:has(.condition-trait:not(.layout-form-group)) {
display: block; display: block;
> .control-label { > .control-label {
@@ -55,6 +56,13 @@
} }
} }
// SAFEGUARD: Force label visibility for form-group layout widgets
// This overrides any conflicting rules (including fallback class rules)
// when the widget has layout-form-group class indicating standard form integration
.form-group:has(.layout-form-group) > .control-label {
display: flex !important;
}
// Dropdown overflow fix // Dropdown overflow fix
// When dropdown is open, parent containers must allow overflow // When dropdown is open, parent containers must allow overflow
.panel:has(.target-search-dropdown.show), .panel:has(.target-search-dropdown.show),
@@ -79,3 +87,75 @@
.target-search-wrapper:has(.target-search-dropdown.show) { .target-search-wrapper:has(.target-search-dropdown.show) {
overflow: visible !important; overflow: visible !important;
} }
// =============================================================================
// Embedded Layout
// =============================================================================
// Use .layout-embedded for entity selectors nested inside other components
// Removes outer wrapper styling to avoid redundant borders/backgrounds
.target-conditions-trait.layout-embedded,
.entity-selector-trait.layout-embedded {
background: transparent;
border: none;
border-radius: 0;
// Remove padding from groups container when embedded
.groups-container {
padding: 0;
}
// Remove block body padding
.block-body {
padding: 0;
}
// Remove block footer border when embedded
.block-footer {
border-top: none;
padding: $es-spacing-sm 0 0;
}
// Simplify selection group when embedded - single thin border only
.selection-group {
background: $es-white;
border: 1px solid $es-slate-200;
border-radius: $es-radius-md;
// Lighter group header in embedded mode
.group-header {
background: $es-slate-50;
border-bottom-color: $es-slate-200;
padding: $es-spacing-xs $es-spacing-sm;
border-radius: $es-radius-md $es-radius-md 0 0;
}
// Reduce group body padding (slightly more than $es-spacing-sm for readability)
.group-body {
padding: 0.75rem;
}
// Reduce group-include section padding
.group-include {
padding: $es-spacing-xs;
margin-bottom: $es-spacing-sm;
}
// Smaller modifiers section
.group-modifiers {
padding: $es-spacing-xs $es-spacing-sm;
margin: $es-spacing-sm (-$es-spacing-sm) (-$es-spacing-sm);
}
}
// Empty state - smaller padding
.groups-empty-state {
padding: $es-spacing-md;
}
// Smaller add group button
.btn-add-group {
padding: 0.375rem 0.625rem;
font-size: $es-font-size-xs;
}
}