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();
}
},

View File

@@ -3,6 +3,7 @@
* Entity chips, selection pills, tags
*/
@use "sass:color";
@use '../variables' 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-name {
// Show full name, no truncation
@@ -344,7 +396,7 @@
.entity-chip.chip-warning {
background: $es-warning-light;
color: darken($es-warning, 20%);
color: color.adjust($es-warning, $lightness: -20%);
&:hover {
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
// PrestaShop admin uses #content .mpr-config-form... with high specificity
// We need to match or exceed that specificity

View File

@@ -208,6 +208,29 @@
height: 100%;
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 {

View File

@@ -306,3 +306,64 @@
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()
.form-group:has(.entity-selector-trait),
.form-group:has(.target-conditions-trait),
.form-group:has(.condition-trait) {
// Excludes .layout-form-group which uses standard PrestaShop form layout
.form-group:has(.entity-selector-trait:not(.layout-form-group)),
.form-group:has(.target-conditions-trait:not(.layout-form-group)),
.form-group:has(.condition-trait:not(.layout-form-group)) {
display: block;
> .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
// When dropdown is open, parent containers must allow overflow
.panel:has(.target-search-dropdown.show),
@@ -79,3 +87,75 @@
.target-search-wrapper:has(.target-search-dropdown.show) {
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;
}
}