This commit is contained in:
2025-03-31 20:17:05 +02:00
parent a03df0b268
commit d4d4c0c09d
1617 changed files with 1106381 additions and 268 deletions

View File

@@ -0,0 +1,124 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ProductMap from '@pages/product/product-map';
const {$} = window;
/**
* Renders the list of combinations in product edit page
*/
export default class CombinationsGridRenderer {
/**
* @returns {{render: (function(*=): void)}}
*/
constructor() {
this.$combinationsTable = $(ProductMap.combinations.combinationsTable);
this.$combinationsTableBody = $(ProductMap.combinations.combinationsTableBody);
this.$loadingSpinner = $(ProductMap.combinations.loadingSpinner);
this.prototypeTemplate = this.$combinationsTable.data('prototype');
this.prototypeName = this.$combinationsTable.data('prototypeName');
return {
render: (data) => this.render(data),
toggleLoading: (loading) => this.toggleLoading(loading),
};
}
/**
* @param {Object} data expected structure: {combinations: [{Object}, {Object}...], total: {Number}}
*/
render(data) {
this.renderCombinations(data.combinations);
}
/**
* @param {Boolean} loading
*/
toggleLoading(loading) {
this.$loadingSpinner.toggle(loading);
}
/**
* @param {Array} combinations
*
* @private
*/
renderCombinations(combinations) {
this.$combinationsTableBody.empty();
let rowIndex = 0;
combinations.forEach((combination) => {
const $row = $(this.getPrototypeRow(rowIndex));
// fill inputs
const $combinationCheckbox = $(ProductMap.combinations.tableRow.combinationCheckbox(rowIndex), $row);
const $combinationIdInput = $(ProductMap.combinations.tableRow.combinationIdInput(rowIndex), $row);
const $combinationNameInput = $(ProductMap.combinations.tableRow.combinationNameInput(rowIndex), $row);
const $quantityInput = $(ProductMap.combinations.tableRow.quantityInput(rowIndex), $row);
const $impactOnPriceInput = $(ProductMap.combinations.tableRow.impactOnPriceInput(rowIndex), $row);
const $referenceInput = $(ProductMap.combinations.tableRow.referenceInput(rowIndex), $row);
// @todo final price should be calculated based on price impact and product price,
// so it doesnt need to be in api response
const $finalPriceInput = $(ProductMap.combinations.tableRow.finalPriceTeInput(rowIndex), $row);
$combinationIdInput.val(combination.id);
$combinationNameInput.val(combination.name);
// This adds the ID in the checkbox label
$combinationCheckbox.closest('label').append(combination.id);
// This adds a text after the cell children (do not use text which replaces everything)
$combinationNameInput.closest('td').append(combination.name);
$finalPriceInput.closest('td').append(combination.finalPriceTe);
$referenceInput.val(combination.reference);
$referenceInput.data('initial-value', combination.reference);
$quantityInput.val(combination.quantity);
$quantityInput.data('initial-value', combination.quantity);
$impactOnPriceInput.val(combination.impactOnPrice);
$impactOnPriceInput.data('initial-value', combination.impactOnPrice);
$(ProductMap.combinations.tableRow.editButton(rowIndex), $row).data('id', combination.id);
$(ProductMap.combinations.tableRow.deleteButton(rowIndex), $row).data('id', combination.id);
$(ProductMap.combinations.tableRow.combinationImg, $row)
.attr('src', combination.imageUrl)
.attr('alt', combination.name);
if (combination.isDefault) {
$(ProductMap.combinations.tableRow.isDefaultInput(rowIndex), $row).prop('checked', true);
}
this.$combinationsTableBody.append($row);
rowIndex += 1;
});
}
/**
* @param {Number} rowIndex
*
* @returns {String}
*
* @private
*/
getPrototypeRow(rowIndex) {
return this.prototypeTemplate.replace(new RegExp(this.prototypeName, 'g'), rowIndex);
}
}

View File

@@ -0,0 +1,437 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ProductMap from '@pages/product/product-map';
import CombinationsGridRenderer from '@pages/product/edit/combinations-grid-renderer';
import CombinationsService from '@pages/product/services/combinations-service';
import DynamicPaginator from '@components/pagination/dynamic-paginator';
import SubmittableInput from '@components/form/submittable-input';
import ProductEventMap from '@pages/product/product-event-map';
import initCombinationModal from '@pages/product/components/combination-modal';
import initFilters from '@pages/product/components/filters';
import ConfirmModal from '@components/modal';
import initCombinationGenerator from '@pages/product/components/generator';
import {getProductAttributeGroups} from '@pages/product/services/attribute-groups';
const {$} = window;
const CombinationEvents = ProductEventMap.combinations;
const CombinationsMap = ProductMap.combinations;
export default class CombinationsManager {
/**
* @param {int} productId
* @returns {{}}
*/
constructor(productId) {
this.productId = productId;
this.eventEmitter = window.prestashop.instance.eventEmitter;
this.$productForm = $(ProductMap.productForm);
this.$combinationsContainer = $(
ProductMap.combinations.combinationsContainer,
);
this.combinationIdInputsSelector = ProductMap.combinations.combinationIdInputsSelector;
this.$externalCombinationTab = $(
ProductMap.combinations.externalCombinationTab,
);
this.$preloader = $(ProductMap.combinations.preloader);
this.$paginatedList = $(CombinationsMap.combinationsPaginatedList);
this.$emptyState = $(CombinationsMap.emptyState);
this.paginator = null;
this.combinationsRenderer = null;
this.filtersApp = null;
this.combinationModalApp = null;
this.combinationGeneratorApp = null;
this.initialized = false;
this.combinationsService = new CombinationsService(this.productId);
this.productAttributeGroups = [];
this.init();
return {};
}
/**
* @private
*/
init() {
// Paginate to first page when tab is shown
this.$productForm
.find(CombinationsMap.navigationTab)
.on('shown.bs.tab', () => this.showCombinationTab());
this.$productForm
.find(CombinationsMap.navigationTab)
.on('hidden.bs.tab', () => this.hideCombinationTab());
// Finally watch events related to combination listing
this.watchEvents();
}
/**
* @private
*/
showCombinationTab() {
this.$externalCombinationTab.removeClass('d-none');
this.firstInit();
}
/**
* @private
*/
hideCombinationTab() {
this.$externalCombinationTab.addClass('d-none');
}
/**
* @private
*/
firstInit() {
if (this.initialized) {
return;
}
this.initialized = true;
this.combinationGeneratorApp = initCombinationGenerator(
CombinationsMap.combinationsGeneratorContainer,
this.eventEmitter,
this.productId,
);
this.combinationModalApp = initCombinationModal(
CombinationsMap.editModal,
this.productId,
this.eventEmitter,
);
this.filtersApp = initFilters(
CombinationsMap.combinationsFiltersContainer,
this.eventEmitter,
this.productAttributeGroups,
);
this.initPaginatedList();
this.refreshCombinationList(true);
}
/**
* @param {boolean} firstTime
* @returns {Promise<void>}
*
* @private
*/
async refreshCombinationList(firstTime) {
// Preloader is only shown on first load
this.$preloader.toggleClass('d-none', !firstTime);
this.$paginatedList.toggleClass('d-none', firstTime);
this.$emptyState.addClass('d-none');
// When attributes are refreshed we show first page
this.paginator.paginate(1);
// Wait for product attributes to adapt rendering depending on their number
this.productAttributeGroups = await getProductAttributeGroups(
this.productId,
);
this.filtersApp.filters = this.productAttributeGroups;
this.eventEmitter.emit(CombinationEvents.clearFilters);
this.$preloader.addClass('d-none');
const hasCombinations = this.productAttributeGroups && this.productAttributeGroups.length;
this.$paginatedList.toggleClass('d-none', !hasCombinations);
if (!hasCombinations) {
// Empty list
this.combinationsRenderer.render({combinations: []});
this.$emptyState.removeClass('d-none');
}
}
/**
* @private
*/
refreshPage() {
this.paginator.paginate(this.paginator.getCurrentPage());
}
/**
* @private
*/
initPaginatedList() {
this.combinationsRenderer = new CombinationsGridRenderer();
this.paginator = new DynamicPaginator(
CombinationsMap.paginationContainer,
this.combinationsService,
this.combinationsRenderer,
);
this.initSubmittableInputs();
this.$combinationsContainer.on(
'change',
CombinationsMap.isDefaultInputsSelector,
async (e) => {
if (!e.currentTarget.checked) {
return;
}
await this.updateDefaultCombination(e.currentTarget);
},
);
this.$combinationsContainer.on(
'click',
CombinationsMap.removeCombinationSelector,
async (e) => {
await this.removeCombination(e.currentTarget);
},
);
this.initSortingColumns();
this.paginator.paginate(1);
}
/**
* @private
*/
watchEvents() {
/* eslint-disable */
this.eventEmitter.on(CombinationEvents.refreshCombinationList, () =>
this.refreshCombinationList(false)
);
this.eventEmitter.on(CombinationEvents.refreshPage, () =>
this.refreshPage()
);
/* eslint-disable */
this.eventEmitter.on(
CombinationEvents.updateAttributeGroups,
attributeGroups => {
const currentFilters = this.combinationsService.getFilters();
currentFilters.attributes = {};
Object.keys(attributeGroups).forEach(attributeGroupId => {
currentFilters.attributes[attributeGroupId] = [];
const attributes = attributeGroups[attributeGroupId];
attributes.forEach(attribute => {
currentFilters.attributes[attributeGroupId].push(attribute.id);
});
});
this.combinationsService.setFilters(currentFilters);
this.paginator.paginate(1);
}
);
this.eventEmitter.on(CombinationEvents.combinationGeneratorReady, () => {
const $generateButtons = $(
ProductMap.combinations.generateCombinationsButton
);
$generateButtons.prop('disabled', false);
$('body').on(
'click',
ProductMap.combinations.generateCombinationsButton,
event => {
// Stop event or it will be caught by click-outside directive and automatically close the modal
event.stopImmediatePropagation();
this.eventEmitter.emit(CombinationEvents.openCombinationsGenerator);
}
);
});
}
/**
* @private
*/
initSubmittableInputs() {
const combinationToken = this.getCombinationToken();
const { quantityKey } = CombinationsMap.combinationItemForm;
const { impactOnPriceKey } = CombinationsMap.combinationItemForm;
const { referenceKey } = CombinationsMap.combinationItemForm;
const { tokenKey } = CombinationsMap.combinationItemForm;
/* eslint-disable */
new SubmittableInput(CombinationsMap.quantityInputWrapper, input =>
this.combinationsService.updateListedCombination(
this.findCombinationId(input),
{
[quantityKey]: input.value,
[tokenKey]: combinationToken
}
)
);
new SubmittableInput(CombinationsMap.impactOnPriceInputWrapper, input =>
this.combinationsService.updateListedCombination(
this.findCombinationId(input),
{
[impactOnPriceKey]: input.value,
[tokenKey]: combinationToken
}
)
);
new SubmittableInput(CombinationsMap.referenceInputWrapper, input =>
this.combinationsService.updateListedCombination(
this.findCombinationId(input),
{
[referenceKey]: input.value,
[tokenKey]: combinationToken
}
)
);
/* eslint-enable */
}
/**
* @private
*/
initSortingColumns() {
this.$combinationsContainer.on(
'click',
CombinationsMap.sortableColumns,
(event) => {
const $sortableColumn = $(event.currentTarget);
const columnName = $sortableColumn.data('sortColName');
if (!columnName) {
return;
}
let direction = $sortableColumn.data('sortDirection');
if (!direction || direction === 'desc') {
direction = 'asc';
} else {
direction = 'desc';
}
// Reset all columns, we need to force the attributes for CSS matching
$(
CombinationsMap.sortableColumns,
this.$combinationsContainer,
).removeData('sortIsCurrent');
$(
CombinationsMap.sortableColumns,
this.$combinationsContainer,
).removeData('sortDirection');
$(
CombinationsMap.sortableColumns,
this.$combinationsContainer,
).removeAttr('data-sort-is-current');
$(
CombinationsMap.sortableColumns,
this.$combinationsContainer,
).removeAttr('data-sort-direction');
// Set correct data in current column, we need to force the attributes for CSS matching
$sortableColumn.data('sortIsCurrent', 'true');
$sortableColumn.data('sortDirection', direction);
$sortableColumn.attr('data-sort-is-current', 'true');
$sortableColumn.attr('data-sort-direction', direction);
// Finally update list
this.combinationsService.setOrderBy(columnName, direction);
this.paginator.paginate(1);
},
);
}
/**
* @param {HTMLElement} button
*
* @private
*/
async removeCombination(button) {
try {
const $deleteButton = $(button);
const modal = new ConfirmModal(
{
id: 'modal-confirm-delete-combination',
confirmTitle: $deleteButton.data('modal-title'),
confirmMessage: $deleteButton.data('modal-message'),
confirmButtonLabel: $deleteButton.data('modal-apply'),
closeButtonLabel: $deleteButton.data('modal-cancel'),
confirmButtonClass: 'btn-danger',
closable: true,
},
async () => {
const response = await this.combinationsService.removeCombination(
this.findCombinationId(button),
);
$.growl({message: response.message});
this.eventEmitter.emit(CombinationEvents.refreshCombinationList);
},
);
modal.show();
} catch (error) {
const errorMessage = error.responseJSON
? error.responseJSON.error
: error;
$.growl.error({message: errorMessage});
}
}
/**
* @param {HTMLElement} checkedInput
*
* @private
*/
async updateDefaultCombination(checkedInput) {
const checkedInputs = this.$combinationsContainer.find(
`${CombinationsMap.isDefaultInputsSelector}:checked`,
);
const checkedDefaultId = this.findCombinationId(checkedInput);
await this.combinationsService.updateListedCombination(checkedDefaultId, {
'combination_item[is_default]': checkedInput.value,
'combination_item[_token]': this.getCombinationToken(),
});
$.each(checkedInputs, (index, input) => {
if (this.findCombinationId(input) !== checkedDefaultId) {
$(input).prop('checked', false);
}
});
}
/**
* @returns {String}
*/
getCombinationToken() {
return $(CombinationsMap.combinationsContainer).data('combinationToken');
}
/**
* @param {HTMLElement} input of the same table row
*
* @returns {Number}
*
* @private
*/
findCombinationId(input) {
return $(input)
.closest('tr')
.find(this.combinationIdInputsSelector)
.val();
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ProductMap from '@pages/product/product-map';
import ProductEventMap from '@pages/product/product-event-map';
import ConfirmModal from '@components/modal';
const {$} = window;
export default class CustomizationsManager {
constructor() {
this.$customizationsContainer = $(ProductMap.customizations.customizationsContainer);
this.$customizationFieldsList = $(ProductMap.customizations.customizationFieldsList);
this.eventEmitter = window.prestashop.instance.eventEmitter;
this.prototypeTemplate = this.$customizationFieldsList.data('prototype');
this.prototypeName = this.$customizationFieldsList.data('prototypeName');
this.init();
}
init() {
this.$customizationsContainer.on('click', ProductMap.customizations.addCustomizationBtn, () => {
this.addCustomizationField();
});
this.$customizationsContainer.on('click', ProductMap.customizations.removeCustomizationBtn, (e) => {
this.removeCustomizationField(e);
});
}
addCustomizationField() {
const index = this.getIndex();
const newItem = this.prototypeTemplate.replace(new RegExp(this.prototypeName, 'g'), this.getIndex());
this.$customizationFieldsList.append(newItem);
window.prestaShopUiKit.initToolTips();
const {translatableInput} = window.prestashop.instance;
translatableInput.refreshFormInputs(this.$customizationsContainer.closest('form'));
this.eventEmitter.emit(ProductEventMap.customizations.rowAdded, {index});
}
removeCustomizationField(event) {
const $deleteButton = $(event.currentTarget);
const modal = new ConfirmModal(
{
id: 'modal-confirm-delete-customization',
confirmTitle: $deleteButton.data('modal-title'),
confirmMessage: $deleteButton.data('modal-message'),
confirmButtonLabel: $deleteButton.data('modal-apply'),
closeButtonLabel: $deleteButton.data('modal-cancel'),
confirmButtonClass: 'btn-danger',
closable: true,
},
() => {
$deleteButton
.closest(ProductMap.customizations.customizationFieldRow)
.remove();
this.eventEmitter.emit(ProductEventMap.customizations.rowRemoved);
},
);
modal.show();
}
getIndex() {
return this.$customizationFieldsList.find(ProductMap.customizations.customizationFieldRow).length;
}
}

View File

@@ -0,0 +1,139 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ProductMap from '@pages/product/product-map';
import Router from '@components/router';
import ConfirmModal from '@components/modal';
import ProductEventMap from '@pages/product/product-event-map';
const {$} = window;
export default class FeatureValuesManager {
/**
* @param eventEmitter {EventEmitter}
*/
constructor(eventEmitter) {
this.router = new Router();
this.eventEmitter = eventEmitter;
this.$collectionContainer = $(ProductMap.featureValues.collectionContainer);
this.$collectionRowsContainer = $(ProductMap.featureValues.collectionRowsContainer);
this.watchFeatureSelectors();
this.watchCustomInputs();
this.watchDeleteButtons();
this.watchAddButton();
}
watchAddButton() {
$(ProductMap.featureValues.addFeatureValue).on('click', () => {
const prototype = this.$collectionContainer.data('prototype');
const prototypeName = this.$collectionContainer.data('prototypeName');
const newIndex = $(ProductMap.featureValues.collectionRow, this.$collectionContainer).length;
const $newRow = $(prototype.replace(new RegExp(prototypeName, 'g'), newIndex));
this.$collectionRowsContainer.append($newRow);
$('select[data-toggle="select2"]', $newRow).select2();
});
}
watchDeleteButtons() {
$(this.$collectionContainer).on('click', ProductMap.featureValues.deleteFeatureValue, (event) => {
const $deleteButton = $(event.currentTarget);
const $collectionRow = $deleteButton.closest(ProductMap.featureValues.collectionRow);
const modal = new ConfirmModal(
{
id: 'modal-confirm-delete-feature-value',
confirmTitle: $deleteButton.data('modal-title'),
confirmMessage: $deleteButton.data('modal-message'),
confirmButtonLabel: $deleteButton.data('modal-apply'),
closeButtonLabel: $deleteButton.data('modal-cancel'),
confirmButtonClass: 'btn-danger',
closable: true,
},
() => {
$collectionRow.remove();
this.eventEmitter.emit(ProductEventMap.updateSubmitButtonState);
},
);
modal.show();
});
}
watchCustomInputs() {
$(this.$collectionContainer).on('keyup change', ProductMap.featureValues.customValueInput, (event) => {
const $changedInput = $(event.target);
const $collectionRow = $changedInput.closest(ProductMap.featureValues.collectionRow);
// Check if any custom inputs has a value
let hasCustomValue = false;
$(ProductMap.featureValues.customValueInput, $collectionRow).each((index, input) => {
const $input = $(input);
if ($input.val() !== '') {
hasCustomValue = true;
}
});
const $featureValueSelector = $(ProductMap.featureValues.featureValueSelect, $collectionRow).first();
$featureValueSelector.prop('disabled', hasCustomValue);
if (hasCustomValue) {
$featureValueSelector.val('');
}
});
}
watchFeatureSelectors() {
$(this.$collectionContainer).on('change', ProductMap.featureValues.featureSelect, (event) => {
const $selector = $(event.target);
const idFeature = $selector.val();
const $collectionRow = $selector.closest(ProductMap.featureValues.collectionRow);
const $featureValueSelector = $(ProductMap.featureValues.featureValueSelect, $collectionRow).first();
const $customValueInputs = $(ProductMap.featureValues.customValueInput, $collectionRow);
const $customFeatureIdInput = $(ProductMap.featureValues.customFeatureIdInput, $collectionRow);
// Reset values
$customValueInputs.val('');
$featureValueSelector.val('');
$customFeatureIdInput.val('');
$.get(this.router.generate('admin_feature_get_feature_values', {idFeature}))
.then((featureValuesData) => {
$featureValueSelector.prop('disabled', featureValuesData.length === 0);
$featureValueSelector.empty();
$.each(featureValuesData, (index, featureValue) => {
// The placeholder shouldn't be posted.
if (featureValue.id === '0') {
featureValue.id = '';
}
$featureValueSelector
.append($('<option></option>')
.attr('value', featureValue.id)
.text(featureValue.value));
});
});
});
}
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import NavbarHandler from '@components/navbar-handler';
import ProductMap from '@pages/product/product-map';
import CategoriesManager from '@pages/product/components/categories';
import CombinationsManager from '@pages/product/edit/combinations-manager';
import CustomizationsManager from '@pages/product/edit/customizations-manager';
import FeatureValuesManager from '@pages/product/edit/feature-values-manager';
import ProductFooterManager from '@pages/product/edit/product-footer-manager';
import ProductFormModel from '@pages/product/edit/product-form-model';
import ProductModulesManager from '@pages/product/edit/product-modules-manager';
import ProductPartialUpdater from '@pages/product/edit/product-partial-updater';
import ProductSEOManager from '@pages/product/edit/product-seo-manager';
import ProductSuppliersManager from '@pages/product/edit/product-suppliers-manager';
import ProductTypeManager from '@pages/product/edit/product-type-manager';
import VirtualProductManager from '@pages/product/edit/virtual-product-manager';
import initDropzone from '@pages/product/components/dropzone';
import initTabs from '@pages/product/components/nav-tabs';
const {$} = window;
$(() => {
window.prestashop.component.initComponents([
'TranslatableField',
'TinyMCEEditor',
'TranslatableInput',
'EventEmitter',
'TextWithLengthCounter',
]);
const $productForm = $(ProductMap.productForm);
const productId = parseInt($productForm.data('productId'), 10);
const productType = $productForm.data('productType');
// Responsive navigation tabs
initTabs();
const {eventEmitter} = window.prestashop.instance;
// Init product model along with input watching and syncing
const productFormModel = new ProductFormModel($productForm, eventEmitter);
if (productId && productType === ProductMap.productType.COMBINATIONS) {
// Combinations manager must be initialized BEFORE nav handler, or it won't trigger the pagination if the tab is
// selected on load, it is only initialized when productId exists though (edition mode)
new CombinationsManager(productId);
}
new NavbarHandler(ProductMap.navigationBar);
new ProductSEOManager();
// Product type has strong impact on the page rendering so when it is modified it must be submitted right away
new ProductTypeManager($(ProductMap.productTypeSelector), $productForm);
new CategoriesManager(eventEmitter);
new ProductFooterManager();
new ProductModulesManager();
const $productFormSubmitButton = $(ProductMap.productFormSubmitButton);
new ProductPartialUpdater(
eventEmitter,
$productForm,
$productFormSubmitButton,
).watch();
// Form has no productId data means that we are in creation mode
if (!productId) {
return;
}
// From here we init component specific to edition
initDropzone(ProductMap.dropzoneImagesContainer);
new FeatureValuesManager(eventEmitter);
new CustomizationsManager();
if (productType !== ProductMap.productType.COMBINATIONS) {
new ProductSuppliersManager(ProductMap.suppliers.productSuppliers, true);
}
if (productType === ProductMap.productType.VIRTUAL) {
new VirtualProductManager(productFormModel);
}
});

View File

@@ -0,0 +1,54 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ConfirmModal from '@components/modal';
import ProductMap from '@pages/product/product-map';
export default class ProductFooterManager {
constructor() {
this.$deleteProductButton = $(ProductMap.footer.deleteProductButton);
this.$deleteProductButton.click(() => this.deleteProduct());
}
deleteProduct() {
const modal = new ConfirmModal(
{
id: 'modal-confirm-delete-product',
confirmTitle: this.$deleteProductButton.data('modal-title'),
confirmMessage: this.$deleteProductButton.data('modal-message'),
confirmButtonLabel: this.$deleteProductButton.data('modal-apply'),
closeButtonLabel: this.$deleteProductButton.data('modal-cancel'),
confirmButtonClass: 'btn-danger',
closable: true,
},
() => {
const removeUrl = this.$deleteProductButton.data('removeUrl');
$(ProductMap.productFormSubmitButton).prop('disabled', true);
window.location = removeUrl;
},
);
modal.show();
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
export default {
'product.stock.quantity': [
'product[stock][quantities][quantity]',
'product[shortcuts][stock][quantity]',
],
'product.price.priceTaxExcluded': [
'product[pricing][retail_price][price_tax_excluded]',
'product[shortcuts][retail_price][price_tax_excluded]',
],
'product.price.priceTaxIncluded': [
'product[pricing][retail_price][price_tax_included]',
'product[shortcuts][retail_price][price_tax_included]',
],
'product.price.taxRulesGroupId': [
'product[pricing][tax_rules_group_id]',
'product[shortcuts][retail_price][tax_rules_group_id]',
],
'product.stock.hasVirtualProductFile': 'product[stock][virtual_product_file][has_file]',
};

View File

@@ -0,0 +1,135 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import BigNumber from 'bignumber.js';
import ObjectFormMapper from '@components/form/form-object-mapper';
import ProductFormMapping from '@pages/product/edit/product-form-mapping';
import ProductEventMap from '@pages/product/product-event-map';
export default class ProductFormModel {
constructor($form, eventEmitter) {
this.eventEmitter = eventEmitter;
// Init form mapper
this.mapper = new ObjectFormMapper(
$form,
ProductFormMapping,
eventEmitter,
{
modelUpdated: ProductEventMap.productModelUpdated,
updateModel: ProductEventMap.updatedProductModel,
modelFieldUpdated: ProductEventMap.updatedProductField,
},
);
// For now we get precision only in the component, but maybe it would deserve a more global configuration
// BigNumber.set({DECIMAL_PLACES: someConfig}) But where can we define/inject this global config?
const $priceTaxExcludedInput = this.mapper.getInputsFor('product.price.priceTaxExcluded');
this.precision = $priceTaxExcludedInput.data('displayPricePrecision');
// Listens to event for product modification (registered after the model is constructed, because events are
// triggered during the initial parsing but don't need them at first).
this.eventEmitter.on(ProductEventMap.updatedProductField, (event) => this.productFieldUpdated(event));
return {
getProduct: () => this.getProduct(),
watch: (productModelKey, callback) => this.watchProductModel(productModelKey, callback),
};
}
/**
* @returns {Object}
*/
getProduct() {
return this.mapper.getModel().product;
}
/**
* @param {string} productModelKey
* @param {function} callback
*/
watchProductModel(productModelKey, callback) {
this.mapper.watch(`product.${productModelKey}`, callback);
}
/**
* Handles modifications that have happened in the product
*
* @param {Object} event
* @private
*/
productFieldUpdated(event) {
this.updateProductPrices(event);
}
/**
* Specific handler for modifications related to the product price
*
* @param {Object} event
* @private
*/
updateProductPrices(event) {
const pricesFields = [
'product.price.priceTaxIncluded',
'product.price.priceTaxExcluded',
'product.price.taxRulesGroupId',
];
if (!pricesFields.includes(event.modelKey)) {
return;
}
const $taxRulesGroupIdInput = this.mapper.getInputsFor('product.price.taxRulesGroupId');
const $selectedTaxOption = $(':selected', $taxRulesGroupIdInput);
let taxRate;
try {
taxRate = new BigNumber($selectedTaxOption.data('taxRate'));
} catch (error) {
taxRate = BigNumber.NaN;
}
if (taxRate.isNaN()) {
taxRate = new BigNumber(0);
}
const taxRatio = taxRate.dividedBy(100).plus(1);
switch (event.modelKey) {
case 'product.price.priceTaxIncluded': {
const priceTaxIncluded = new BigNumber(this.getProduct().price.priceTaxIncluded);
this.mapper.set(
'product.price.priceTaxExcluded',
priceTaxIncluded.dividedBy(taxRatio).toFixed(this.precision),
);
break;
}
default: {
const priceTaxExcluded = new BigNumber(this.getProduct().price.priceTaxExcluded);
this.mapper.set('product.price.priceTaxIncluded', priceTaxExcluded.times(taxRatio).toFixed(this.precision));
break;
}
}
}
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ProductMap from '@pages/product/product-map';
export default class ProductModulesManager {
constructor() {
this.$previewContainer = $(ProductMap.modules.previewContainer);
this.$selectorContainer = $(ProductMap.modules.selectorContainer);
this.$contentContainer = $(ProductMap.modules.contentContainer);
this.$moduleSelector = $(ProductMap.modules.moduleSelector);
this.$selectorPreviews = $(ProductMap.modules.selectorPreviews);
this.$moduleContents = $(ProductMap.modules.moduleContents);
this.init();
return {};
}
/**
* @private
*/
init() {
this.$previewContainer.removeClass('d-none');
this.$selectorContainer.addClass('d-none');
this.$contentContainer.addClass('d-none');
this.$selectorPreviews.addClass('d-none');
this.$moduleContents.addClass('d-none');
this.$previewContainer.on('click', ProductMap.modules.previewButton, (event) => {
const $button = $(event.target);
this.selectModule($button.data('target'));
});
this.$moduleSelector.on('change', () => this.showSelectedModule());
}
/**
* @param {string} moduleId
*
* @private
*/
selectModule(moduleId) {
this.$previewContainer.addClass('d-none');
this.$selectorContainer.removeClass('d-none');
this.$contentContainer.removeClass('d-none');
this.$moduleSelector.val(moduleId);
// trigger change because this is a select2 component, and module is switched when change even triggers
this.$moduleSelector.trigger('change');
}
/**
* @private
*/
showSelectedModule() {
this.$selectorPreviews.addClass('d-none');
this.$moduleContents.addClass('d-none');
const moduleId = this.$moduleSelector.val();
$(ProductMap.modules.selectorPreview(moduleId)).removeClass('d-none');
$(ProductMap.modules.moduleContent(moduleId)).removeClass('d-none');
}
}

View File

@@ -0,0 +1,262 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import _ from 'lodash';
import ProductEventMap from '@pages/product/product-event-map';
const {$} = window;
/**
* When product is edited we want to send only partial updates
* so this class compares the initial data from the form computes
* the diff when form is submitted And dynamically build another
* form to submit only updated data (along with required fields
* token and such).
*
* It also disabled the submit button as long as no data has been
* modified by the user.
*/
export default class ProductPartialUpdater {
/**
* @param eventEmitter {EventEmitter}
* @param $productForm {jQuery}
* @param $productFormSubmitButton {jQuery}
*/
constructor(eventEmitter, $productForm, $productFormSubmitButton) {
this.eventEmitter = eventEmitter;
this.$productForm = $productForm;
this.$productFormSubmitButton = $productFormSubmitButton;
}
/**
* This the public method you need to use to start this component
* ex: new ProductPartialUpdater($productForm, $productFormSubmitButton).watch();
*/
watch() {
// Avoid submitting form when pressing Enter
this.$productForm.keypress((e) => e.which !== 13);
this.$productFormSubmitButton.prop('disabled', true);
this.initialData = this.getFormDataAsObject();
this.$productForm.submit(() => this.updatePartialForm());
// 'dp.change' event allows tracking datepicker input changes
this.$productForm.on('keyup change dp.change', ':input', () => this.updateSubmitButtonState());
this.eventEmitter.on(ProductEventMap.updateSubmitButtonState, () => this.updateSubmitButtonState());
this.watchCustomizations();
this.initFormattedTextarea();
}
/**
* Watch events specifically related to customizations subform
*/
watchCustomizations() {
this.eventEmitter.on(ProductEventMap.customizations.rowAdded, () => this.updateSubmitButtonState());
this.eventEmitter.on(ProductEventMap.customizations.rowRemoved, () => this.updateSubmitButtonState());
}
/**
* Rich editors apply a layer over initial textarea fields therefore they need to be watched differently.
*/
initFormattedTextarea() {
this.eventEmitter.on('tinymceEditorSetup', (event) => {
event.editor.on('change', () => this.updateSubmitButtonState());
});
}
/**
* This methods handles the form submit
*
* @returns {boolean}
*
* @private
*/
updatePartialForm() {
const updatedData = this.getUpdatedFormData();
if (updatedData !== null) {
let formMethod = this.$productForm.prop('method');
if (Object.prototype.hasOwnProperty.call(updatedData, '_method')) {
// eslint-disable-next-line dot-notation
formMethod = updatedData['_method'];
}
if (formMethod !== 'PATCH') {
// Returning true will continue submitting form as usual
return true;
}
// On patch method we extract changed values and submit only them
this.submitUpdatedData(updatedData);
} else {
// @todo: This is temporary we should probably use a nice modal instead, that said since the submit button is
// disabled when no data has been modified it should never happen
alert('no fields updated');
}
return false;
}
/**
* Dynamically build a form with provided updated data and submit this "shadow" form
*
* @param updatedData {Object} Contains an object with all form fields to update indexed by query parameters name
*/
submitUpdatedData(updatedData) {
this.$productFormSubmitButton.prop('disabled', true);
const $updatedForm = this.createShadowForm(updatedData);
$updatedForm.appendTo('body');
$updatedForm.submit();
}
/**
* @param updatedData
*
* @returns {Object} Form clone (Jquery object)
*/
createShadowForm(updatedData) {
const $updatedForm = this.$productForm.clone();
$updatedForm.empty();
$updatedForm.prop('class', '');
Object.keys(updatedData).forEach((fieldName) => {
if (Array.isArray(updatedData[fieldName])) {
updatedData[fieldName].forEach((value) => {
this.appendInputToForm($updatedForm, fieldName, value);
});
} else {
this.appendInputToForm($updatedForm, fieldName, updatedData[fieldName]);
}
});
return $updatedForm;
}
/**
* Adapt the submit button state, as long as no data has been updated the button is disabled
*/
updateSubmitButtonState() {
const updatedData = this.getUpdatedFormData();
this.$productFormSubmitButton.prop('disabled', updatedData === null);
}
/**
* Returns the updated data, only fields which are different from the initial page load
* are returned (token and method are added since they are required for a valid request).
*
* If no fields have been modified this method returns null.
*
* @returns {{}|null}
*/
getUpdatedFormData() {
const currentData = this.getFormDataAsObject();
// Loop through current form data and remove the one that did not change
// This way only updated AND new values remain
Object.keys(this.initialData).forEach((fieldName) => {
const fieldValue = this.initialData[fieldName];
// Field is absent in the new data (it was not in the initial) we force it to empty string (not null
// or it will be ignored)
if (!Object.prototype.hasOwnProperty.call(currentData, fieldName)) {
currentData[fieldName] = '';
} else if (_.isEqual(currentData[fieldName], fieldValue)) {
delete currentData[fieldName];
}
});
// No need to loop through the field contained in currentData and not in the initial
// they are new values so are, by fact, updated values
if (Object.keys(currentData).length === 0) {
return null;
}
// Some parameters are always needed
const permanentParameters = [
// We need the form CSRF token
'product[_token]',
// If method is not POST or GET a hidden type input is used to simulate it (like PATCH)
'_method',
];
permanentParameters.forEach((permanentParameter) => {
if (Object.prototype.hasOwnProperty.call(this.initialData, permanentParameter)) {
currentData[permanentParameter] = this.initialData[permanentParameter];
}
});
return currentData;
}
/**
* Returns the serialized form data as an Object indexed by field name
*
* @returns {{}}
*/
getFormDataAsObject() {
const formArray = this.$productForm.serializeArray();
const serializedForm = {};
formArray.forEach((formField) => {
let {value} = formField;
// Input names can be identical when expressing array of values for same field (like multiselect checkboxes)
// so we need to put these input values into single array indexed by that field name
if (formField.name.endsWith('[]')) {
let multiField = [];
if (Object.prototype.hasOwnProperty.call(serializedForm, formField.name)) {
multiField = serializedForm[formField.name];
}
multiField.push(formField.value);
value = multiField;
}
serializedForm[formField.name] = value;
});
// File inputs must be handled manually
$('input[type="file"]', this.$productForm).each((inputIndex, fileInput) => {
$.each($(fileInput)[0].files, (fileIndex, file) => {
serializedForm[fileInput.name] = file;
});
});
return serializedForm;
}
/**
* @param $form
* @param name
* @param value
*
* @private
*/
appendInputToForm($form, name, value) {
$('<input>').attr({
name,
type: 'hidden',
value,
}).appendTo($form);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import Serp from '@app/utils/serp';
import RedirectOptionManager from '@pages/product/edit/redirect-option-manager';
import ProductMap from '@pages/product/product-map';
const {$} = window;
export default class ProductSEOManager {
constructor() {
this.$previewButton = $(ProductMap.footer.previewUrlButton);
this.init();
return {};
}
/**
* @private
*/
init() {
// Init the product/category search field for redirection target
const $redirectTypeInput = $(ProductMap.seo.redirectOption.typeInput);
const $redirectTargetInput = $(ProductMap.seo.redirectOption.targetInput);
new RedirectOptionManager($redirectTypeInput, $redirectTargetInput);
// Init Serp component to preview Search engine display
const {translatableInput, translatableField} = window.prestashop.instance;
let previewUrl = this.$previewButton.data('seoUrl');
if (!previewUrl) {
previewUrl = '';
}
new Serp(
{
container: ProductMap.seo.container,
defaultTitle: ProductMap.seo.defaultTitle,
watchedTitle: ProductMap.seo.watchedTitle,
defaultDescription: ProductMap.seo.defaultDescription,
watchedDescription: ProductMap.seo.watchedDescription,
watchedMetaUrl: ProductMap.seo.watchedMetaUrl,
multiLanguageInput: `${translatableInput.localeInputSelector}:not(.d-none)`,
multiLanguageField: `${translatableField.translationFieldSelector}.active`,
},
previewUrl,
);
}
}

View File

@@ -0,0 +1,265 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import SuppliersMap from '@pages/product/suppliers-map';
const {$} = window;
export default class ProductSuppliersManager {
/**
*
* @param {string} suppliersFormId
* @param {boolean} forceUpdateDefault
*
* @returns {{}}
*/
constructor(suppliersFormId, forceUpdateDefault) {
this.forceUpdateDefault = forceUpdateDefault;
this.suppliersMap = SuppliersMap(suppliersFormId);
this.$productSuppliersCollection = $(this.suppliersMap.productSuppliersCollection);
this.$supplierIdsGroup = $(this.suppliersMap.supplierIdsInput).closest('.form-group');
this.$defaultSupplierGroup = $(this.suppliersMap.defaultSupplierInput).closest('.form-group');
this.$productsTable = $(this.suppliersMap.productSuppliersTable);
this.$productsTableBody = $(this.suppliersMap.productsSuppliersTableBody);
this.suppliers = [];
this.prototypeTemplate = this.$productSuppliersCollection.data('prototype');
this.prototypeName = this.$productSuppliersCollection.data('prototypeName');
this.defaultDataForSupplier = this.getDefaultDataForSupplier();
this.init();
return {};
}
init() {
this.memorizeCurrentSuppliers();
this.toggleTableVisibility();
this.refreshDefaultSupplierBlock();
this.$initialDefault = this.$defaultSupplierGroup.find('input:checked').first();
if (this.$initialDefault.length) {
this.$initialDefault
.closest(this.suppliersMap.checkboxContainer)
.addClass(this.suppliersMap.defaultSupplierClass);
}
this.$productsTable.on('change', 'input', () => {
this.memorizeCurrentSuppliers();
});
this.$supplierIdsGroup.on('change', 'input', (e) => {
const input = e.currentTarget;
if (input.checked) {
this.addSupplier({
supplierId: input.value,
supplierName: input.dataset.label,
});
} else {
this.removeSupplier(input.value);
}
this.renderSuppliers();
this.toggleTableVisibility();
this.refreshDefaultSupplierBlock();
});
}
toggleTableVisibility() {
if (this.getSelectedSuppliers().length === 0) {
this.hideTable();
return;
}
this.showTable();
}
/**
* @param {Object} supplier
*/
addSupplier(supplier) {
if (typeof this.suppliers[supplier.supplierId] === 'undefined') {
const newSupplier = Object.create(this.defaultDataForSupplier);
newSupplier.supplierId = supplier.supplierId;
newSupplier.supplierName = supplier.supplierName;
this.suppliers[supplier.supplierId] = newSupplier;
} else {
this.suppliers[supplier.supplierId].removed = false;
}
}
/**
* @param {int} supplierId
*/
removeSupplier(supplierId) {
this.suppliers[supplierId].removed = true;
}
renderSuppliers() {
this.$productsTableBody.empty();
// Loop through select suppliers so that we use the same order as in the select list
this.getSelectedSuppliers().forEach((selectedSupplier) => {
const supplier = this.suppliers[selectedSupplier.supplierId];
if (supplier.removed) {
return;
}
const productSupplierRow = this.prototypeTemplate.replace(
new RegExp(this.prototypeName, 'g'),
supplier.supplierId,
);
this.$productsTableBody.append(productSupplierRow);
// Fill inputs
const rowMap = this.suppliersMap.productSupplierRow;
$(rowMap.supplierIdInput(supplier.supplierId)).val(supplier.supplierId);
$(rowMap.supplierNamePreview(supplier.supplierId)).html(supplier.supplierName);
$(rowMap.supplierNameInput(supplier.supplierId)).val(supplier.supplierName);
$(rowMap.productSupplierIdInput(supplier.supplierId)).val(supplier.productSupplierId);
$(rowMap.referenceInput(supplier.supplierId)).val(supplier.reference);
$(rowMap.priceInput(supplier.supplierId)).val(supplier.price);
$(rowMap.currencyIdInput(supplier.supplierId)).val(supplier.currencyId);
});
}
getSelectedSuppliers() {
const selectedSuppliers = [];
this.$supplierIdsGroup.find('input:checked').each((index, input) => {
selectedSuppliers.push({
supplierName: input.dataset.label,
supplierId: input.value,
});
});
return selectedSuppliers;
}
refreshDefaultSupplierBlock() {
const suppliers = this.getSelectedSuppliers();
if (suppliers.length === 0) {
if (this.forceUpdateDefault) {
this.$defaultSupplierGroup.find('input').prop('checked', false);
}
this.hideDefaultSuppliers();
return;
}
this.showDefaultSuppliers();
const selectedSupplierIds = suppliers.map((supplier) => supplier.supplierId);
this.$defaultSupplierGroup.find('input').each((key, input) => {
const isValid = selectedSupplierIds.includes(input.value);
if (this.forceUpdateDefault && !isValid) {
input.checked = false;
}
input.disabled = !isValid;
});
if (this.$defaultSupplierGroup.find('input:checked').length === 0 && this.forceUpdateDefault) {
this.checkFirstAvailableDefaultSupplier(selectedSupplierIds);
}
}
hideDefaultSuppliers() {
this.$defaultSupplierGroup.addClass('d-none');
}
showDefaultSuppliers() {
this.$defaultSupplierGroup.removeClass('d-none');
}
/**
* @param {int[]} selectedSupplierIds
*/
checkFirstAvailableDefaultSupplier(selectedSupplierIds) {
const firstSupplierId = selectedSupplierIds[0];
this.$defaultSupplierGroup.find(`input[value="${firstSupplierId}"]`).prop('checked', true);
}
showTable() {
this.$productsTable.removeClass('d-none');
}
hideTable() {
this.$productsTable.addClass('d-none');
}
/**
* Memorize suppliers to be able to re-render them later.
* Flag `removed` allows identifying whether supplier was removed from list or should be rendered
*/
memorizeCurrentSuppliers() {
this.getSelectedSuppliers().forEach((supplier) => {
this.suppliers[supplier.supplierId] = {
supplierId: supplier.supplierId,
productSupplierId: $(this.suppliersMap.productSupplierRow.productSupplierIdInput(supplier.supplierId)).val(),
supplierName: $(this.suppliersMap.productSupplierRow.supplierNameInput(supplier.supplierId)).val(),
reference: $(this.suppliersMap.productSupplierRow.referenceInput(supplier.supplierId)).val(),
price: $(this.suppliersMap.productSupplierRow.priceInput(supplier.supplierId)).val(),
currencyId: $(this.suppliersMap.productSupplierRow.currencyIdInput(supplier.supplierId)).val(),
removed: false,
};
});
}
/**
* Create a "shadow" prototype just to parse default values set inside the input fields,
* this allow to build an object with default values set in the FormType
*
* @returns {{reference, removed: boolean, price, currencyId, productSupplierId}}
*/
getDefaultDataForSupplier() {
const rowPrototype = new DOMParser().parseFromString(
this.prototypeTemplate,
'text/html',
);
return {
removed: false,
productSupplierId: this.getDataFromRow(this.suppliersMap.productSupplierRow.productSupplierIdInput, rowPrototype),
reference: this.getDataFromRow(this.suppliersMap.productSupplierRow.referenceInput, rowPrototype),
price: this.getDataFromRow(this.suppliersMap.productSupplierRow.priceInput, rowPrototype),
currencyId: this.getDataFromRow(this.suppliersMap.productSupplierRow.currencyIdInput, rowPrototype),
};
}
/**
* @param selectorGenerator {function}
* @param rowPrototype {Document}
*
* @returns {*}
*/
getDataFromRow(selectorGenerator, rowPrototype) {
return rowPrototype.querySelector(selectorGenerator(this.prototypeName)).value;
}
}

View File

@@ -0,0 +1,100 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ConfirmModal from '@components/modal';
import ProductMap from '@pages/product/product-map';
/**
* This component watches for modification of the product type, when it happens it displays a modal warning
* for the user with a warning about what is going to be deleted if he validates this change. If modification
* is confirmed the form is submitted right away to validate the change and update the page.
*/
export default class ProductTypeManager {
/**
* @param {jQuery} $typeSelector Select element to choose the product type
* @param {jQuery} $productForm Product form that needs to be submitted
*/
constructor($typeSelector, $productForm) {
this.$typeSelector = $typeSelector;
this.$productForm = $productForm;
this.productId = parseInt($productForm.data('productId'), 10);
this.initialType = $typeSelector.val();
this.$typeSelector.on('change', (event) => this.confirmTypeSubmit(event));
return {};
}
/**
* @private
*/
confirmTypeSubmit() {
let confirmMessage = this.$typeSelector.data('confirm-message');
let confirmWarning = '';
// If no productId we are in creation page so no need for extra warning
if (this.productId) {
switch (this.initialType) {
case ProductMap.productType.COMBINATIONS:
confirmWarning = this.$typeSelector.data('combinations-warning');
break;
case ProductMap.productType.PACK:
confirmWarning = this.$typeSelector.data('pack-warning');
break;
case ProductMap.productType.VIRTUAL:
confirmWarning = this.$typeSelector.data('virtual-warning');
break;
case ProductMap.productType.STANDARD:
default:
confirmWarning = '';
break;
}
}
if (confirmWarning) {
confirmWarning = `<div class="alert alert-warning">${confirmWarning}</div>`;
}
confirmMessage = `<div class="alert alert-info">${confirmMessage}</div>`;
const modal = new ConfirmModal(
{
id: 'modal-confirm-product-type',
confirmTitle: this.$typeSelector.data('modal-title'),
confirmMessage: `${confirmMessage} ${confirmWarning}`,
confirmButtonLabel: this.$typeSelector.data('modal-apply'),
closeButtonLabel: this.$typeSelector.data('modal-cancel'),
closable: false,
},
() => {
$(ProductMap.productFormSubmitButton).prop('disabled', true);
this.$productForm.submit();
},
() => {
this.$typeSelector.val(this.initialType);
},
);
modal.show();
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import EntitySearchInput from '@components/entity-search-input';
const {$} = window;
/**
* This component is used in product page to selected where the redirection points to when the
* product is out of stock. It is composed on two inputs:
* - a selection of the redirection type
* - a rich component to select a product or a category
*
* When the type is changed the component automatically updates the labels, remote search urls
* and values of the target.
*/
export default class RedirectOptionManager {
constructor($redirectTypeInput, $redirectTargetInput) {
this.$redirectTypeInput = $redirectTypeInput;
this.$redirectTargetInput = $redirectTargetInput;
this.$redirectTargetRow = this.$redirectTargetInput.closest('.form-group');
this.$redirectTargetLabel = $('.form-control-label', this.$redirectTargetRow).first();
this.$redirectTargetHint = $('.typeahead-hint', this.$redirectTargetRow);
this.buildAutoCompleteSearchInput();
this.watchRedirectType();
}
/**
* Watch the selected redirection type and adapt the inputs accordingly.
*/
watchRedirectType() {
this.lastSelectedType = this.$redirectTypeInput.val();
this.$redirectTypeInput.change(() => {
const redirectType = this.$redirectTypeInput.val();
switch (redirectType) {
case '301-category':
case '302-category':
this.entitySearchInput.setRemoteUrl(this.$redirectTargetInput.data('categorySearchUrl'));
this.$redirectTargetInput.prop('placeholder', this.$redirectTargetInput.data('categoryPlaceholder'));
this.$redirectTargetLabel.html(this.$redirectTargetInput.data('categoryLabel'));
// If previous type was not a category we reset the selected value
if (this.lastSelectedType !== '301-category' && this.lastSelectedType !== '302-category') {
this.entitySearchInput.setValue(null);
}
this.$redirectTargetHint.html(this.$redirectTargetInput.data('categoryHelp'));
this.showTarget();
break;
case '301-product':
case '302-product':
this.entitySearchInput.setRemoteUrl(this.$redirectTargetInput.data('productSearchUrl'));
this.$redirectTargetInput.prop('placeholder', this.$redirectTargetInput.data('productPlaceholder'));
this.$redirectTargetLabel.html(this.$redirectTargetInput.data('productLabel'));
// If previous type was not a category we reset the selected value
if (this.lastSelectedType !== '301-product' && this.lastSelectedType !== '302-product') {
this.entitySearchInput.setValue(null);
}
this.$redirectTargetHint.html(this.$redirectTargetInput.data('productHelp'));
this.showTarget();
break;
case '404':
default:
this.entitySearchInput.setValue(null);
this.hideTarget();
break;
}
this.lastSelectedType = this.$redirectTypeInput.val();
});
}
buildAutoCompleteSearchInput() {
this.entitySearchInput = new EntitySearchInput(this.$redirectTargetInput);
}
showTarget() {
this.$redirectTargetRow.removeClass('d-none');
}
hideTarget() {
this.$redirectTargetRow.addClass('d-none');
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
import ProductMap from '@pages/product/product-map';
const {$} = window;
export default class VirtualProductManager {
constructor(productFormModel) {
this.productFormModel = productFormModel;
this.$virtualProductContainer = $(ProductMap.virtualProduct.container);
this.$fileContentContainer = $(ProductMap.virtualProduct.fileContentContainer);
this.init();
return {};
}
/**
* @private
*/
init() {
this.productFormModel.watch('stock.hasVirtualProductFile', () => this.toggleContentVisibility());
this.toggleContentVisibility();
}
toggleContentVisibility() {
const hasVirtualFile = Number(this.productFormModel.getProduct().stock.hasVirtualProductFile) === 1;
const hasErrors = this.$virtualProductContainer
.find(ProductMap.invalidField)
.length !== 0;
if (hasVirtualFile || hasErrors) {
this.showContent();
} else {
this.hideContent();
}
}
/**
* @private
*/
hideContent() {
this.$fileContentContainer.addClass('d-none');
}
/**
* @private
*/
showContent() {
this.$fileContentContainer.removeClass('d-none');
}
}