update
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
107
iadmin/themes/new-theme/js/pages/product/edit/index.js
Normal file
107
iadmin/themes/new-theme/js/pages/product/edit/index.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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]',
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user