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:
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
2
assets/js/admin/entity-selector.min.js
vendored
2
assets/js/admin/entity-selector.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user