Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
export default {
openRangeSelectionModal: 'openRangeSelectionModal',
shippingMethodChange: 'carrierShippingMethodChange',
rangesUpdated: 'carrierRangesUpdated',
};

View File

@@ -0,0 +1,250 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
import {EventEmitter} from 'events';
import CarrierFormMap from '@pages/carrier/form/carrier-form-map';
import CarrierFormEventMap from '@pages/carrier/form/carrier-form-event-map';
import ConfirmModal from '@js/components/modal/confirm-modal';
import {Range} from '@pages/carrier/form/types';
const {$} = window;
/**
* This component is used in carrier form page to manage the behavior of the form:
* - Selections of zones, ranges and ranges prices
* - Update form when the carrier shipping method change
*/
export default class CarrierFormManager {
eventEmitter: EventEmitter;
currentShippingSymbol: string;
$zonesInput: JQuery;
$rangesInput: JQuery;
$shippingMethodInput: JQuery;
$freeShippingInput: JQuery;
/**
* @param {EventEmitter} eventEmitter
*/
constructor(eventEmitter: EventEmitter) {
this.eventEmitter = eventEmitter;
this.currentShippingSymbol = '';
// Initialize dom elements
this.$zonesInput = $(CarrierFormMap.zonesInput);
this.$rangesInput = $(CarrierFormMap.rangesInput);
this.$shippingMethodInput = $(CarrierFormMap.shippingMethodInput);
this.$freeShippingInput = $(CarrierFormMap.freeShippingInput);
// Initialize form
this.initForm();
// Initialize listeners
this.initListeners();
}
private initForm() {
// First toggle shipping related controls
this.refreshFreeShipping();
// Then, we need to refresh the shipping method symbol
this.refreshCurrentShippingSymbol();
this.onChangeZones();
}
private initListeners() {
this.$zonesInput.on('change', () => this.onChangeZones());
this.$freeShippingInput.on('change', () => {
this.refreshFreeShipping();
this.onChangeZones();
});
this.$shippingMethodInput.on('change', () => this.refreshCurrentShippingSymbol());
$(CarrierFormMap.zonesContainer).on('click', CarrierFormMap.deleteZoneButton, (e:Event) => this.onDeleteZone(e));
this.eventEmitter.on(CarrierFormEventMap.rangesUpdated, (ranges: Range[]) => this.onChangeRanges(ranges));
}
private refreshFreeShipping(): void {
const isFreeShipping = $(`${CarrierFormMap.freeShippingInput}:checked`).val() === '1';
CarrierFormMap.shippingControls.forEach((inputId: string) => {
const $inputGroup = $(inputId).closest('.form-group');
$inputGroup.toggleClass('d-none', isFreeShipping);
$(inputId).prop('required', !isFreeShipping);
});
}
private refreshCurrentShippingSymbol() {
// First, we need to get the units of the selected shipping method
const shippingMethodUnits = $(CarrierFormMap.shippingMethodRow).data('units');
const shippingMethodValue = <number> this.$shippingMethodInput.filter(':checked').first().val() || -1;
this.currentShippingSymbol = shippingMethodUnits[shippingMethodValue] || '?';
// Then, we need to emit an event to update this symbol to other components
this.eventEmitter.emit(CarrierFormEventMap.shippingMethodChange, this.currentShippingSymbol);
// Finally, we need to update the ranges names with the new symbol
$(CarrierFormMap.rangeRow).each((_, rangeRow: HTMLElement) => {
const $rangeRow = $(rangeRow);
const $rangeName = $rangeRow.find(CarrierFormMap.rangeNamePreview);
const $rangeNameHidden = $rangeRow.find(CarrierFormMap.rangeNameInput);
const from = $rangeRow.find(CarrierFormMap.rangeFromInput).val();
const to = $rangeRow.find(CarrierFormMap.rangeToInput).val();
const rangeName = `${from}${this.currentShippingSymbol} - ${to}${this.currentShippingSymbol}`;
$rangeName.text(rangeName);
$rangeNameHidden.val(rangeName);
});
}
private onChangeZones() {
// First, we retrieve the zones actually displayed and selected
const $zonesContainer = $(CarrierFormMap.zonesContainer);
const $zonesRows = $(CarrierFormMap.zoneRow);
const zones = <string[]> this.$zonesInput.val() ?? [];
// First, we need to delete the zones that are not selected and already displayed
// (and we keep the zones that are already displayed)
const zonesAlreadyDisplayed = <string[]>[];
$zonesRows.each((_, zoneRow: HTMLElement) => {
const $zoneRow = $(zoneRow);
const zoneId = $zoneRow.find(CarrierFormMap.zoneIdInput).val()?.toString();
if (zoneId !== undefined) {
if (!zones.includes(zoneId)) {
$zoneRow.remove();
} else {
zonesAlreadyDisplayed.push(zoneId);
}
}
});
// Then, we need to add the zones that are selected but not displayed
const zonePrototype = $zonesContainer.data('prototype');
zones.forEach((zoneId: string) => {
if (!zonesAlreadyDisplayed.includes(zoneId)) {
// We create new zone row by duplicating the prototype and replacing the zone index
const prototype = zonePrototype.replace(/__zone__/g, $(CarrierFormMap.zoneRow).length);
// We need to update the zone id and the zone name
const $prototype = $(prototype);
$prototype.attr('data-zone-id', zoneId);
$prototype.find(CarrierFormMap.zoneIdInput).val(zoneId);
$prototype.find(CarrierFormMap.zoneNamePreview).text(this.$zonesInput.find(CarrierFormMap.zoneIdOption(zoneId)).text());
// We append the new zone row into the zones container
$zonesContainer.append($prototype);
// Next, we need to prepare the ranges for this zone
const $rangeContainer = $prototype.find(CarrierFormMap.rangesContainer);
const $rangeContainerBody = $prototype.find(CarrierFormMap.rangesContainerBody);
const rangePrototype = $rangeContainer.data('prototype');
// @ts-ignore
const ranges = <Range[]>JSON.parse(this.$rangesInput.val() || '[]');
// For each range selected, we need to create a new range row with the range prototype
ranges.forEach((range: Range, index) => {
// Then, we append the new range row into the range container
const $rPrototype = this.prepareRangePrototype(rangePrototype, index, range);
$rangeContainerBody.append($rPrototype);
});
}
});
}
private onDeleteZone(e: Event) {
e.preventDefault();
// We need to get the zone id to delete
const $currentTarget = $(e.currentTarget as HTMLElement);
const $currentZoneRow = $currentTarget.parents(CarrierFormMap.zoneRow);
const idZoneToDelete = $currentZoneRow.children(CarrierFormMap.zoneIdInput).val();
// We need to display a confirmation modal before deleting the zone
const modal = new ConfirmModal(
{
id: 'modal-confirm-submit-feature-flag',
confirmButtonClass: 'btn-danger',
confirmTitle: $currentTarget.data('modal-title'),
confirmMessage: '',
confirmButtonLabel: $currentTarget.data('modal-confirm'),
closeButtonLabel: $currentTarget.data('modal-cancel'),
},
() => {
// If, the user confirms the deletion, we need to remove the zone
// First, we need to remove this zone from the zones
let zones = <string[]> this.$zonesInput.val() || [];
zones = zones.filter((zoneId: string) => zoneId !== idZoneToDelete);
// And update the zones selected values and trigger the zones selector change event
this.$zonesInput.val(zones);
this.$zonesInput.change();
},
);
modal.show();
}
private onChangeRanges(ranges: Range[]) {
// We retrieve all ranges containers in the page
const $rangesContainerBodies = $(CarrierFormMap.rangesContainerBody);
// For each range container, we need to update the ranges
$rangesContainerBodies.each((_, zoneRangesContainer: HTMLElement) => {
// First, we need to save all values for this range.
const $zoneRangesContainerBody = $(zoneRangesContainer);
const pricesRanges = $(zoneRangesContainer).find(CarrierFormMap.rangeRow).map((__, rangeRow: HTMLElement) => {
const $rangeRow = $(rangeRow);
const from = parseFloat($rangeRow.find(CarrierFormMap.rangeFromInput).val()?.toString() || '0');
const to = parseFloat($rangeRow.find(CarrierFormMap.rangeToInput).val()?.toString() || '0');
const price = $rangeRow.find(CarrierFormMap.rangePriceInput).val() || '';
return {from, to, price};
});
// Then, we reset the ranges container
$zoneRangesContainerBody.html('');
// and, we need to add all the ranges selected
const rangePrototype = $zoneRangesContainerBody.closest(CarrierFormMap.rangesContainer).data('prototype');
ranges.forEach((range: Range, index) => {
// First, we need to prepare the range prototype
const $rPrototype = this.prepareRangePrototype(rangePrototype, index, range);
// Then, we need to search the previous price if exist (oldFrom = newFrom OR oldTo = newTo)
let price = '';
for (let i = 0; i < pricesRanges.length; i += 1) {
if (pricesRanges[i].from === range.from || pricesRanges[i].to === range.to) {
price = pricesRanges[i].price.toString();
break;
}
}
// We set the previous value for this range if it exists
// @ts-ignore
$rPrototype.find(CarrierFormMap.rangePriceInput)
.attr('data-from', range.from || '0')
.attr('data-to', range.to || '0')
.val(price);
// Then, we append the new range row into the range container
$zoneRangesContainerBody.append($rPrototype);
});
});
}
private prepareRangePrototype(rangePrototype: string, index: number, range: Range): JQuery {
// We prepare the range prototype by replacing the range index, and setting the range values
const $rPrototype = $(rangePrototype.replace(/__range__/g, index.toString()));
$rPrototype.find(CarrierFormMap.rangeFromInput).val(range.from || '0');
$rPrototype.find(CarrierFormMap.rangeToInput).val(range.to || '0');
$rPrototype.find(CarrierFormMap.rangeNamePreview)
.text(`${range.from}${this.currentShippingSymbol} - ${range.to}${this.currentShippingSymbol}`);
// We return the prototype well formed
return $rPrototype;
}
}

View File

@@ -0,0 +1,38 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
export default {
form: 'form[name="carrier"]',
navigationBar: '#form-nav',
freeShippingInput: 'input[name="carrier[shipping_settings][is_free]"]',
zonesInput: '#carrier_shipping_settings_zones',
zoneIdOption: (zoneId: number|string): string => `option[value="${zoneId}"]`,
rangesInput: '#carrier_shipping_settings_ranges_data',
rangesSelectionAppId: '#carrier_shipping_settings_ranges-app',
addRangeButton: '.js-add-carrier-ranges-btn',
shippingMethodRow: '#carrier_shipping_settings_shipping_method',
shippingMethodInput: 'input[name="carrier[shipping_settings][shipping_method]"]',
deleteZoneButton: '.js-carrier-delete-zone',
zonesContainer: '#carrier_shipping_settings_ranges_costs',
rangesContainer: '.js-carrier-range-container',
rangesContainerBody: '.js-carrier-range-container-body',
zoneRow: '.js-carrier-zone-row',
zoneIdInput: 'input[name$="[zoneId]"]',
rangeNamePreview: '.js-carrier-range-name .text-preview-value',
rangeNameInput: '.js-carrier-range-name input[type="hidden"]',
rangeRow: '.js-carrier-range-row',
zoneNamePreview: '.card-title .text-preview-value',
rangeFromInput: 'input[name$="[from]"]',
rangeToInput: 'input[name$="[to]"]',
rangePriceInput: 'input[name$="[price]"]',
shippingControls: [
'#carrier_shipping_settings_id_tax_rule_group',
'#carrier_shipping_settings_has_additional_handling_fee',
'#carrier_shipping_settings_shipping_method',
'#carrier_shipping_settings_range_behavior',
'#carrier_shipping_settings_ranges',
'#carrier_shipping_settings_ranges_costs',
],
};

View File

@@ -0,0 +1,58 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
import CarrierFormMap from '@pages/carrier/form/carrier-form-map';
import {createApp} from 'vue';
import {createI18n} from 'vue-i18n';
import CarrierRangesModal from '@pages/carrier/form/components/CarrierRangesModal.vue';
import EventEmitter from '@components/event-emitter';
import ReplaceFormatter from '@PSVue/plugins/vue-i18n/replace-formatter';
import CarrierFormEventMap from '@pages/carrier/form/carrier-form-event-map';
export default class CarrierRanges {
private readonly eventEmitter: typeof EventEmitter;
constructor(eventEmitter: typeof EventEmitter) {
this.eventEmitter = eventEmitter;
this.initRangesSelectionModal();
}
initRangesSelectionModal(): void {
// Create the modal container
const $showModal = $(CarrierFormMap.addRangeButton);
const $modalContainer = $(`<div id="${CarrierFormMap.rangesSelectionAppId.slice(1)}"></div>`);
$showModal.after($modalContainer);
// Retreive translations from the button
const i18n = createI18n({
locale: 'en',
formatter: new ReplaceFormatter(),
messages: {en: $showModal.data('translations')},
});
// Initialize the Vue app with the CarrierRangesModal component
const vueApp = createApp(CarrierRangesModal, {
i18n,
eventEmitter: this.eventEmitter,
}).use(i18n);
// Mount the Vue app to the modal container
vueApp.mount(CarrierFormMap.rangesSelectionAppId);
// Open the modal with data when the button "Add range" is clicked
$showModal.click((e: JQuery.ClickEvent) => {
e.preventDefault();
e.stopImmediatePropagation();
const data = $(CarrierFormMap.rangesInput).val() || '[]';
this.eventEmitter.emit(CarrierFormEventMap.openRangeSelectionModal, JSON.parse(data.toString()));
});
// Listen the modal to apply the ranges selected to the data
this.eventEmitter.on(CarrierFormEventMap.rangesUpdated, (ranges: Array<object>) => {
const $data = $(CarrierFormMap.rangesInput);
$data.val(JSON.stringify(ranges));
});
}
}

View File

@@ -0,0 +1,324 @@
<!--*
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*-->
<template>
<div data-role="carrier-ranges-edit-modal">
<modal
v-if="isModalShown"
:modal-title="$t('modal.title')"
:confirm-label="$t('modal.apply')"
:cancel-label="$t('modal.cancel')"
:confirmation="true"
:close-on-click-outside="false"
@close="cancelChanges"
@confirm="applyChanges"
@mouseleave="mouseLeave"
>
<template #header>
<h5 class="modal-title">
{{ $t('modal.title') }}
</h5>
</template>
<template #body>
<div
class="alert alert-danger"
v-if="overlappingAlert"
role="alert"
>
{{ $t('modal.overlappingAlert') }}
</div>
<div
class="alert alert-danger"
v-if="negativeRangeAlert"
role="alert"
>
{{ $t('modal.negativeRangeAlert') }}
</div>
<div class="table-container">
<table class="table table-carrier-ranges-modal">
<thead>
<tr>
<th />
<th>{{ $t('modal.col.from') }}</th>
<th>{{ $t('modal.col.to') }} </th>
<th>{{ $t('modal.col.action') }}</th>
</tr>
</thead>
<tbody :key="refreshKey">
<template
:key="i"
v-for="r, i in ranges"
>
<tr :data-row="i">
<td>
<button
type="button"
class="btn-add"
@click.prevent="addRange(i)"
>
<i class="material-icons">add</i>
</button>
</td>
<td>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">{{ this.symbol }}</span>
</div>
<input
type="number"
class="form-control form-from"
inputmode="decimal"
v-model.number="r.from"
>
</div>
</td>
<td>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">{{ this.symbol }}</span>
</div>
<input
type="number"
class="form-control form-to"
inputmode="decimal"
v-model.number="r.to"
>
</div>
</td>
<td align="center">
<button
type="button"
@click.prevent="deleteRange(i)"
class="btn-delete"
>
<i class="material-icons">delete</i>
</button>
</td>
</tr>
</template>
</tbody>
</table>
<button
@click.prevent="addRange()"
type="button"
class="btn btn-sm btn-outline-secondary mt-2"
>
<i class="material-icons">add_box</i>
{{ $t('modal.addRange') }}
</button>
</div>
</template>
</modal>
</div>
</template>
<script lang="ts">
import Modal from '@PSVue/components/Modal.vue';
import {defineComponent} from 'vue';
import CarrierFormEventMap from '@pages/carrier/form/carrier-form-event-map';
import {Range} from '@pages/carrier/form/types';
interface CarrierRangesModalStates {
isModalShown: boolean, // define if the modal is shown
ranges: Range[], // define the ranges currently displayed
savedRanges: Range[], // define the ranges saved before the changes
refreshKey: number, // force the refresh of the table by incrementing this key
errors: boolean, // define if there are errors in the ranges
overlappingAlert: boolean, // define if there are overlapping ranges (and display an alert)
symbol: string, // define the current symbol used in function of the shipping method
}
export default defineComponent({
name: 'CarrierRangesModal',
components: {Modal},
data(): CarrierRangesModalStates {
return {
isModalShown: false,
ranges: [],
savedRanges: [],
refreshKey: 0,
errors: false,
overlappingAlert: false,
negativeRangeAlert: false,
symbol: '',
};
},
props: {
eventEmitter: {
type: Object,
required: true,
},
},
mounted() {
// If we need to open this modal
this.eventEmitter.on(CarrierFormEventMap.openRangeSelectionModal, (ranges: Range[]) => {
this.ranges = ranges ?? [];
this.openModal();
});
// If we need to change the shipping method symbol
this.eventEmitter.on(CarrierFormEventMap.shippingMethodChange, (symbol: string) => { this.symbol = symbol; });
},
methods: {
openModal() {
// We add a class to the body to prevent scrolling
document.querySelector('body')?.classList.add('overflow-hidden');
this.isModalShown = true;
// We save the ranges to be able to cancel the changes
this.savedRanges.splice(0, this.savedRanges.length);
this.ranges.forEach((range) => this.savedRanges.push({from: range.from, to: range.to}));
// We add an empty range if there is none
if (this.ranges.length === 0) {
this.ranges.push({from: null, to: null});
}
// We reset the errors
this.errors = false;
this.overlappingAlert = false;
this.negativeRangeAlert = false;
},
closeModal() {
// We remove the class to allow scrolling
document.querySelector('body')?.classList.remove('overflow-hidden');
this.isModalShown = false;
this.refreshKey = 0;
},
cancelChanges() {
// We cancel the changes and close the modal
this.ranges.splice(0, this.ranges.length);
this.savedRanges.forEach((range) => this.ranges.push({from: range.from, to: range.to}));
// We remove empty ranges
this.ranges = this.ranges.filter((range) => range.from !== null || range.to !== null);
// Then, we close the modal
this.closeModal();
},
applyChanges() {
// We remove empty ranges
this.ranges = this.ranges.filter((range) => range.from !== null || range.to !== null);
// We validate the changes
this.validateChanges();
if (!this.errors) {
// We emit the new ranges
this.eventEmitter.emit(CarrierFormEventMap.rangesUpdated, this.ranges);
// We close the modal
this.closeModal();
}
},
validateChanges() {
const table = <HTMLElement>document.querySelector('.table-carrier-ranges-modal');
// Reset errors
this.errors = false;
this.overlappingAlert = false;
this.negativeRangeAlert = false;
// We remove the error class from all inputs already in error
table.querySelectorAll('input.is-invalid').forEach((input) => {
input.classList.remove('is-invalid');
});
// We sort the ranges by min values
this.ranges.sort((a, b) => (a.from || 0) - (b.from || 0));
// We check ranges
let saveMax: null | number = null;
this.ranges.forEach((range, index) => {
// Check if all fields are filled and are not negative
if (range.from === null || typeof range.from === 'string' || range.from < 0) {
table.querySelectorAll(`tr[data-row="${index}"] input.form-from`)
.forEach((input) => {
input.classList.add('is-invalid');
});
this.errors = true;
this.negativeRangeAlert = true;
}
if (range.to === null || typeof range.to === 'string' || range.to < 0) {
table.querySelectorAll(`tr[data-row="${index}"] input.form-to`)
.forEach((input) => {
input.classList.add('is-invalid');
});
this.errors = true;
this.negativeRangeAlert = true;
}
// Check overlapping
if (saveMax !== null && range.from !== null && range.from < saveMax) {
table.querySelectorAll(`tr[data-row="${index - 1}"] input.form-to`)
.forEach((input) => {
input.classList.add('is-invalid');
});
table.querySelectorAll(`tr[data-row="${index}"] input.form-from`)
.forEach((input) => {
input.classList.add('is-invalid');
});
this.errors = true;
this.overlappingAlert = true;
}
// Check from < to for each range
if (range.to !== null && range.from !== null && range.to <= range.from) {
table.querySelectorAll(`tr[data-row="${index}"] input.form-to`)
.forEach((input) => {
input.classList.add('is-invalid');
});
this.errors = true;
}
saveMax = range.to;
});
},
addRange(index: undefined | number) {
// Add new range at the index specified, at the bottom if not specified
// (with "from" already set to the previous "to")
if (index === undefined) {
this.ranges.push({from: this.ranges[this.ranges.length - 1]?.to, to: null});
} else {
this.ranges.splice(index + 1, 0, {from: this.ranges[index]?.to, to: null});
}
},
deleteRange(rangeIndex: number) {
// We remove the selected range
this.ranges.splice(rangeIndex, 1);
// We add an empty range if there is none
if (this.ranges.length === 0) {
this.ranges.push({from: null, to: null});
}
},
},
});
</script>
<style lang="scss" type="text/scss" scoped>
@import '~@scss/config/_settings.scss';
.modal {
.modal-footer {
justify-content: space-between;
}
.table {
margin-bottom: 0;
border-bottom: 0;
tr td {
border: 0;
}
}
.btn-delete,
.btn-add {
border: none;
background: none;
i {
font-size: 1.2em;
}
}
.table-container {
max-height: 60vh;
overflow-y: auto;
}
}
</style>

View File

@@ -0,0 +1,38 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
import ChoiceTable from '@js/components/choice-table';
import NavbarHandler from '@js/components/navbar-handler';
import CarrierFormManager from '@pages/carrier/form/carrier-form-manager';
import CarrierRanges from '@pages/carrier/form/carrier-range-modal';
import CarrierFormMap from '@pages/carrier/form/carrier-form-map';
import NavbarFormErrorHandler from '@js/components/navbar-form-error-handler';
$(() => {
// Initialize components
window.prestashop.component.initComponents([
'TranslatableInput',
'EventEmitter',
'MultipleZoneChoice',
'ChoiceTable',
]);
// Initialize the ranges selection modal
new CarrierRanges(window.prestashop.instance.eventEmitter);
new ChoiceTable();
// Initialize the carrier form manager
new CarrierFormManager(window.prestashop.instance.eventEmitter);
const carrierForm = document.querySelector(CarrierFormMap.form);
if (carrierForm instanceof HTMLElement) {
new NavbarFormErrorHandler({
form: carrierForm,
navbarHandler: new NavbarHandler($(CarrierFormMap.navigationBar)),
});
}
});

View File

@@ -0,0 +1,9 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
export interface Range {
from: number|null,
to: number|null,
}

View File

@@ -0,0 +1,28 @@
/**
* For the full copyright and license information, please view the
* docs/licenses/LICENSE.txt file that was distributed with this source code.
*/
import ShowcaseCard from '@components/showcase-card/showcase-card';
import ShowcaseCardCloseExtension from '@components/showcase-card/extension/showcase-card-close-extension';
const {$} = window;
$(() => {
const carrierGrid = new window.prestashop.component.Grid('carrier');
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.SortingExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.ReloadListExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.PositionExtension(carrierGrid));
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.FiltersSubmitButtonEnablerExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.FiltersResetExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.ExportToSqlManagerExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.ColumnTogglingExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.LinkRowActionExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.SubmitRowActionExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.SubmitBulkActionExtension());
carrierGrid.addExtension(new window.prestashop.component.GridExtensions.BulkActionCheckboxExtension());
const showcaseCard = new ShowcaseCard('carriersShowcaseCard');
showcaseCard.addExtension(new ShowcaseCardCloseExtension());
});