/*
* Pim
* Free Extension
* Copyright (c) TreoLabs GmbH
* Copyright (c) Kenner Soft Service GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Espo.define('pim:views/product/record/panels/product-attribute-values', ['views/record/panels/relationship', 'views/record/panels/bottom', 'search-manager'],
(Dep, BottomPanel, SearchManager) => Dep.extend({
template: 'pim:product/record/panels/product-attribute-values',
baseSelectFields: [
'channelsIds',
'channelsNames',
'data',
'attributeGroupId',
'attributeGroupName',
'attributeId',
'attributeName',
'isRequired',
'productFamilyAttributeId',
'scope',
'value'
],
groupKey: 'attributeGroupId',
groupLabel: 'attributeGroupName',
groupScope: 'AttributeGroup',
groupsWithoutId: ['no_group'],
noGroup: {
key: 'no_group',
label: 'No Group'
},
initialAttributes: null,
boolFilterData: {
notLinkedProductAttributeValues() {
return {
productId: this.model.id,
scope: 'Global'
}
}
},
events: _.extend({
'click [data-action="unlinkAttributeGroup"]': function (e) {
e.preventDefault();
e.stopPropagation();
let data = $(e.currentTarget).data();
this.unlinkAttributeGroup(data);
}
}, Dep.prototype.events),
data() {
return _.extend({
groups: this.groups,
groupScope: this.groupScope
}, Dep.prototype.data.call(this));
},
setup() {
let bottomPanel = new BottomPanel();
bottomPanel.setup.call(this);
this.link = this.link || this.defs.link || this.panelName;
if (!this.scope && !(this.link in this.model.defs.links)) {
throw new Error('Link \'' + this.link + '\' is not defined in model \'' + this.model.name + '\'');
}
this.title = this.title || this.translate(this.link, 'links', this.model.name);
this.scope = this.scope || this.model.defs.links[this.link].entity;
if (!this.getConfig().get('scopeColorsDisabled')) {
var iconHtml = this.getHelper().getScopeColorIconHtml(this.scope);
if (iconHtml) {
if (this.defs.label) {
this.titleHtml = iconHtml + this.translate(this.defs.label, 'labels', this.scope);
} else {
this.titleHtml = iconHtml + this.title;
}
}
}
var url = this.url || this.model.name + '/' + this.model.id + '/' + this.link;
if (!this.readOnly && !this.defs.readOnly) {
if (!('create' in this.defs)) {
this.defs.create = true;
}
if (!('select' in this.defs)) {
this.defs.select = true;
}
}
this.filterList = this.defs.filterList || this.filterList || null;
if (this.filterList && this.filterList.length) {
this.filter = this.getStoredFilter();
}
if (this.defs.create && this.getAcl().check('ProductAttributeValue', 'create')) {
this.buttonList.push({
title: 'Create',
action: this.defs.createAction || 'createRelated',
link: this.link,
acl: 'create',
aclScope: this.scope,
html: '<span class="fas fa-plus"></span>',
data: {
link: this.link,
}
});
}
if (this.defs.select && this.getAcl().check('ProductAttributeValue', 'create')) {
var data = {link: this.link};
if (this.defs.selectPrimaryFilterName) {
data.primaryFilterName = this.defs.selectPrimaryFilterName;
}
if (this.defs.selectBoolFilterList) {
data.boolFilterList = this.defs.selectBoolFilterList;
}
data.boolFilterListCallback = 'getSelectBoolFilterList';
data.boolFilterDataCallback = 'getSelectBoolFilterData';
data.afterSelectCallback = 'createProductAttributeValue';
data.scope = 'Attribute';
this.actionList.unshift({
label: 'Select',
action: this.defs.selectAction || 'selectRelated',
data: data,
acl: 'edit',
aclScope: this.model.name
});
if (this.getAcl().check('AttributeGroup', 'read')) {
this.actionList.push({
label: 'Select Attribute Group',
action: 'selectAttributeGroup'
});
}
}
this.setupActions();
var layoutName = 'listSmall';
this.setupListLayout();
if (this.listLayoutName) {
layoutName = this.listLayoutName;
}
var listLayout = null;
var layout = this.defs.layout || null;
if (layout) {
if (typeof layout == 'string') {
layoutName = layout;
} else {
layoutName = 'listRelationshipCustom';
listLayout = layout;
}
}
this.layoutName = layoutName;
this.listLayout = listLayout;
var sortBy = this.defs.sortBy || null;
var asc = this.defs.asc || null;
if (this.defs.orderBy) {
sortBy = this.defs.orderBy;
asc = true;
if (this.defs.orderDirection) {
if (this.defs.orderDirection && (this.defs.orderDirection === true || this.defs.orderDirection.toLowerCase() === 'DESC')) {
asc = false;
}
}
}
this.wait(true);
this.getCollectionFactory().create(this.scope, function (collection) {
collection.maxSize = 200;
if (this.defs.filters) {
var searchManager = new SearchManager(collection, 'listRelationship', false, this.getDateTime());
searchManager.setAdvanced(this.defs.filters);
collection.where = searchManager.getWhere();
}
collection.url = collection.urlRoot = url;
if (sortBy) {
collection.sortBy = sortBy;
}
if (asc) {
collection.asc = asc;
}
this.collection = collection;
this.setFilter(this.filter);
this.updateBaseSelectFields();
this.listenTo(this.model, 'updateAttributes change:productFamilyId update-all after:relate after:unrelate after:attributesSave', link => {
if (!link || link === 'productAttributeValues') {
this.actionRefresh();
}
});
if (this.getMetadata().get(['scopes', this.model.name, 'advancedFilters'])) {
this.listenTo(this.model, 'overview-filters-changed', () => {
this.applyOverviewFilters();
});
}
this.getMetadata().fetch();
this.fetchCollectionGroups(() => this.wait(false));
}, this);
this.setupFilterActions();
},
updateBaseSelectFields() {
(this.defs.baseSelectFields || []).forEach(field => {
if (!this.baseSelectFields.includes(field)) {
this.baseSelectFields.push(field);
}
});
let inputLanguageList = this.getConfig().get('inputLanguageList') || [];
if (this.getConfig().get('isMultilangActive') && inputLanguageList.length) {
inputLanguageList.forEach(lang => {
let field = lang.split('_').reduce((prev, curr) => prev + Espo.Utils.upperCaseFirst(curr.toLocaleLowerCase()), 'value');
if (!this.baseSelectFields.includes(field)) {
this.baseSelectFields.push(field);
}
});
}
},
createProductAttributeValue(selectObj) {
let promises = [];
selectObj.forEach(attributeModel => {
this.getModelFactory().create(this.scope, model => {
model.setRelate({
model: this.model,
link: this.model.defs.links[this.link].foreign
});
model.setRelate({
model: attributeModel,
link: attributeModel.defs.links[this.link].foreign
});
let attributes = {
assignedUserId: this.getUser().id,
assignedUserName: this.getUser().get('name'),
scope: 'Global'
};
model.set(attributes);
promises.push(model.save());
});
});
Promise.all(promises).then(() => {
this.notify('Linked', 'success');
this.model.trigger('after:relate', 'productAttributeValues');
});
},
actionSelectAttributeGroup() {
const scope = 'AttributeGroup';
const viewName = this.getMetadata().get(['clientDefs', scope, 'modalViews', 'select']) || 'views/modals/select-records';
this.notify('Loading...');
this.createView('dialog', viewName, {
scope: scope,
multiple: true,
createButton: false,
massRelateEnabled: false,
boolFilterList: ['withNotLinkedAttributesToProduct'],
boolFilterData: {withNotLinkedAttributesToProduct: this.model.id},
whereAdditional: [
{
type: 'isLinked',
attribute: 'attributes'
}
]
}, dialog => {
dialog.render();
this.notify(false);
dialog.once('select', selectObj => {
this.notify('Loading...');
if (!Array.isArray(selectObj)) {
return;
}
let boolFilterList = this.getSelectBoolFilterList() || [];
this.getFullEntityList('Attribute', {
where: [
{
type: 'bool',
value: boolFilterList,
data: this.getSelectBoolFilterData(boolFilterList)
},
{
attribute: 'attributeGroupId',
type: 'in',
value: selectObj.map(model => model.id)
}
]
}, list => {
let models = [];
list.forEach(attributes => {
this.getModelFactory().create('Attribute', model => {
model.set(attributes);
models.push(model);
});
});
this.createProductAttributeValue(models);
});
});
});
},
getFullEntityList(url, params, callback, container) {
if (url) {
container = container || [];
let options = params || {};
options.maxSize = options.maxSize || 200;
options.offset = options.offset || 0;
this.ajaxGetRequest(url, options).then(response => {
container = container.concat(response.list || []);
options.offset = container.length;
if (response.total > container.length || response.total === -1) {
this.getFullEntity(url, options, callback, container);
} else {
callback(container);
}
});
}
},
afterRender() {
Dep.prototype.afterRender.call(this);
this.buildGroups();
if (this.mode === 'edit') {
this.setEditMode();
}
},
fetchCollectionGroups(callback) {
this.collection.data.select = this.getSelectFields().join(',');
this.collection.reset();
this.fetchCollectionPart(() => {
this.groups = [];
this.groups = this.getGroupsFromCollection();
let valueKeys = this.groups.map(group => group.key);
this.getCollectionFactory().create('AttributeGroup', collection => {
this.attributeGroupCollection = collection;
collection.select = 'sortOrder';
collection.maxSize = 200;
collection.offset = 0;
collection.whereAdditional = [
{
attribute: 'id',
type: 'in',
value: valueKeys
}
];
collection.fetch().then(() => {
this.applySortingForAttributeGroups();
if (callback) {
callback();
}
});
});
});
},
applySortingForAttributeGroups() {
this.groups.forEach(item => {
let sortOder = 0;
let attributeGroup = this.attributeGroupCollection.find(model => model.id === item.key);
if (attributeGroup) {
sortOder = attributeGroup.get('sortOrder');
}
item.sortOrder = item.sortOrder || sortOder;
});
let noGroup = this.groups.find(item => item.key === 'no_group');
if (noGroup) {
noGroup.sortOrder = Math.max(...this.groups.map(group => group.sortOrder)) + 1;
}
this.groups.sort(function (a, b) {
return a.sortOrder - b.sortOrder;
});
},
getSelectFields() {
return this.baseSelectFields || [];
},
fetchCollectionPart(callback) {
this.collection.fetch({remove: false, more: true}).then((response) => {
if (this.collection.total > this.collection.length) {
this.fetchCollectionPart(callback);
} else if (callback) {
callback();
}
});
},
getGroupsFromCollection() {
let groups = [];
this.collection.forEach(model => {
let params = this.getGroupParams(model);
this.setGroup(params, model, groups);
});
return groups;
},
getGroupParams(model) {
let key = model.get(this.groupKey);
if (!key) {
key = this.noGroup.key;
}
let label = model.get(this.groupLabel);
if (!label) {
label = this.translate(this.noGroup.label, 'labels', 'Product');
}
return {
key: key,
label: label
};
},
setGroup(params, model, groups) {
let group = groups.find(item => item.key === params.key);
if (group) {
group.editable = group.editable && model.get('isCustom');
group.rowList.push(model.id);
group.rowList.sort((a, b) => this.collection.get(a).get('sortOrder') - this.collection.get(b).get('sortOrder'));
} else {
groups.push({
key: params.key,
id: !this.groupsWithoutId.includes(params.key) ? params.key : null,
label: params.label,
rowList: [model.id],
editable: model.get('isCustom')
});
}
},
buildGroups() {
let count = 0;
this.groups.forEach(group => {
this.getCollectionFactory().create(this.scope, collection => {
group.rowList.forEach(id => {
collection.add(this.collection.get(id));
});
this.setGroupCollectionDefs(group, collection);
this.listenTo(collection, 'sync', () => {
this.initialAttributes = this.getInitialAttributes();
this.model.trigger('attributes-updated');
collection.models.sort((a, b) => a.get('sortOrder') - b.get('sortOrder'));
if (this.getMetadata().get(['scopes', this.model.name, 'advancedFilters'])) {
this.applyOverviewFilters();
}
});
let viewName = this.defs.recordListView || this.getMetadata().get('clientDefs.' + this.scope + '.recordViews.list') || 'Record.List';
let options = {
collection: collection,
layoutName: this.layoutName,
listLayout: this.listLayout,
checkboxes: false,
rowActionsView: this.defs.readOnly ? false : (this.defs.rowActionsView || this.rowActionsView),
buttonsDisabled: true,
el: `${this.options.el} .group[data-name="${group.key}"] .list-container`,
showMore: false
};
this.createView(group.key, viewName, this.modifyListOptions(options), view => {
if (this.getMetadata().get(['scopes', this.model.name, 'advancedFilters'])) {
view.listenTo(view, 'after:render', () => this.applyOverviewFilters());
}
view.render(() => {
count++;
if (count === this.groups.length) {
this.trigger('groups-rendered');
}
});
});
});
});
},
setGroupCollectionDefs(group, collection) {
collection.url = `Product/${this.model.id}/productAttributeValues`;
collection.where = [
{
type: 'bool',
value: ['linkedWithAttributeGroup'],
data: {
linkedWithAttributeGroup: {
productId: this.model.id,
attributeGroupId: group.key !== 'no_group' ? group.key : null
}
}
}
];
collection.data.select = this.getSelectFields().join(',');
},
modifyListOptions(options) {
return options;
},
applyOverviewFilters() {
let currentFieldFilter = (this.model.advancedEntityView || {}).fieldsFilter;
let attributesWithChannelScope = [];
let fields = this.getValueFields();
Object.keys(fields).forEach(name => {
let fieldView = fields[name];
let hide = !this.checkFieldValue(currentFieldFilter, fieldView.model.get(fieldView.name), fieldView.model.get('isRequired'));
if (!hide) {
hide = this.updateCheckByChannelFilter(fieldView, attributesWithChannelScope);
}
if (!hide && this.getConfig().get('isMultilangActive') && (this.getConfig().get('inputLanguageList') || []).length) {
hide = this.updateCheckByLocaleFilter(fieldView, currentFieldFilter);
}
this.controlRowVisibility(fieldView, name, hide);
});
this.hideChannelAttributesWithGlobalScope(fields, attributesWithChannelScope);
},
updateCheckByChannelFilter(fieldView, attributesWithChannelScope) {
let hide = false;
let currentChannelFilter = (this.model.advancedEntityView || {}).channelsFilter;
if (currentChannelFilter) {
if (currentChannelFilter === 'onlyGlobalScope') {
hide = fieldView.model.get('scope') !== 'Global';
} else {
hide = (fieldView.model.get('scope') === 'Channel' && !(fieldView.model.get('channelsIds') || []).includes(currentChannelFilter));
if ((fieldView.model.get('channelsIds') || []).includes(currentChannelFilter)) {
attributesWithChannelScope.push(fieldView.model.get('attributeId'));
}
}
}
return hide;
},
updateCheckByLocaleFilter(fieldView, currentFieldFilter) {
let isShowGeneric = (this.model.advancedEntityView || {}).showGenericFields;
if (!isShowGeneric && fieldView.model.get('locale') === null) {
return true;
}
let localeFilter = (this.model.advancedEntityView || {}).localesFilter;
let fieldLocale = fieldView.model.get('locale');
if (localeFilter !== null && localeFilter !== '' && localeFilter !== fieldLocale && fieldLocale !== null) {
return true;
}
return false;
},
getValueFields() {
let fields = {};
this.groups.forEach(group => {
let groupView = this.getView(group.key);
if (groupView) {
groupView.rowList.forEach(row => {
let rowView = groupView.getView(row);
if (rowView) {
let containerView = rowView.getView('valueField');
if (containerView) {
let fieldView = containerView.getView('valueField');
fieldView.groupKey = group.key;
fields[row] = fieldView;
}
}
});
}
});
return fields;
},
checkFieldValue(currentFieldFilter, value, required) {
let check = !currentFieldFilter;
if (currentFieldFilter === 'empty') {
check = value === null || value === '' || (Array.isArray(value) && !value.length);
}
if (currentFieldFilter === 'emptyAndRequired') {
check = (value === null || value === '' || (Array.isArray(value) && !value.length)) && required;
}
return check;
},
controlRowVisibility(fieldView, rowId, hide) {
let groupView = this.getView(fieldView.groupKey);
let rowView = groupView.getView(rowId);
if (hide) {
rowView.$el.addClass('hidden');
} else {
rowView.$el.removeClass('hidden');
}
this.controlGroupVisibility(groupView);
},
controlGroupVisibility(groupView) {
if (groupView.$el.find('.list-row.hidden').size() === (groupView.rowList || []).length) {
groupView.$el.parent().addClass('hidden');
} else {
groupView.$el.parent().removeClass('hidden');
}
},
hideChannelAttributesWithGlobalScope(fields, attributesWithChannelScope) {
Object.keys(fields).forEach(name => {
let fieldView = fields[name];
if (attributesWithChannelScope.includes(fieldView.model.get('attributeId')) && fieldView.model.get('scope') === 'Global') {
this.controlRowVisibility(fieldView, name, true);
}
});
},
getSelectBoolFilterData(boolFilterList) {
let data = {};
if (Array.isArray(boolFilterList)) {
boolFilterList.forEach(item => {
if (this.boolFilterData && typeof this.boolFilterData[item] === 'function') {
data[item] = this.boolFilterData[item].call(this);
}
});
}
return data;
},
getSelectBoolFilterList() {
return this.defs.selectBoolFilterList || null
},
actionRefresh() {
Object.keys(this.nestedViews).forEach(view => this.clearView(view));
this.getMetadata().fetch();
this.fetchCollectionGroups(() => {
this.reRender();
});
},
unlinkAttributeGroup(data) {
let id = data.id;
if (!id) {
return;
}
let group = this.groups.find(group => group.id == id);
if (!group || !group.rowList) {
return;
}
this.confirm({
message: this.translate('unlinkAttributeGroupConfirmation', 'messages', 'AttributeGroup'),
confirmText: this.translate('Unlink')
}, function () {
this.notify('Unlinking...');
$.ajax({
url: `${this.model.name}/${this.link}/relation`,
data: JSON.stringify({
ids: [this.model.id],
foreignIds: group.rowList
}),
type: 'DELETE',
contentType: 'application/json',
success: function () {
this.notify('Unlinked', 'success');
this.model.trigger('after:unrelate');
this.actionRefresh();
}.bind(this),
error: function () {
this.notify('Error occurred', 'error');
}.bind(this),
});
}, this);
},
setListMode() {
this.mode = 'list';
this.groups.forEach(group => {
let groupView = this.getView(group.key);
if (groupView) {
groupView.setListMode();
}
});
this.reRender();
},
setEditMode() {
this.initialAttributes = this.getInitialAttributes();
const groupsRendered = this.groups.every(group => {
const groupView = this.getView(group.key);
return groupView && groupView.isRendered();
});
const updateMode = () => {
this.mode = 'edit';
this.groups.forEach(group => {
let groupView = this.getView(group.key);
if (groupView) {
groupView.setEditMode();
}
});
};
if (groupsRendered) {
updateMode();
} else {
this.listenToOnce(this, 'groups-rendered', () => updateMode());
}
},
cancelEdit() {
this.actionRefresh();
},
getInitialAttributes() {
const data = {};
this.collection.forEach(model => {
const modelData = {
value: model.get('value')
};
const actualFields = this.getFieldManager().getActualAttributeList(model.get('attributeType'), 'value');
actualFields.forEach(field => {
if (model.has(field)) {
_.extend(modelData, {[field]: model.get(field)});
}
});
const additionalData = model.get('data');
if (additionalData) {
modelData.data = additionalData;
}
data[model.id] = Espo.Utils.cloneDeep(modelData);
});
return data;
},
panelFetch() {
let data = false;
this.groups.forEach(group => {
const groupView = this.getView(group.key);
(groupView.rowList || []).forEach(id => {
const row = groupView.getView(id);
const value = row.getView('valueField');
if (value.mode === 'edit') {
const fetchedData = value.fetch();
const initialData = this.initialAttributes[id];
value.model.set(fetchedData);
if (this.equalityValueCheck(fetchedData, initialData)) {
data = _.extend(data || {}, {[id]: fetchedData});
}
}
});
});
return data;
},
equalityValueCheck(fetchedData, initialData) {
return !_.isEqual(initialData, fetchedData) && (Object.keys(fetchedData).every(key => {
const initial = initialData[key];
const fetched = fetchedData[key];
return (Array.isArray(initial) ? initial.length : initial) || (Array.isArray(fetched) ? fetched.length : fetched);
}));
},
save() {
const data = this.panelFetch();
if (data) {
const promises = [];
$.each(data, (id, attrs) => {
this.collection.get(id).set(attrs, {silent: true});
promises.push(this.ajaxPutRequest(`${this.collection.name}/${id}`, attrs))
});
this.notify('Saving...');
Promise.all(promises)
.then(response => {
this.notify('Saved', 'success');
this.model.trigger('after:attributesSave');
}, error => {
this.actionRefresh();
});
}
},
validate() {
this.trigger('collapsePanel', 'show');
let notValid = false;
this.groups.forEach(group => {
const groupView = this.getView(group.key);
(groupView.rowList || []).forEach(id => {
const row = groupView.getView(id);
const value = row.getView('valueField');
if (value.mode === 'edit' && !value.disabled && !value.readOnly) {
notValid = value.validate() || notValid;
}
});
});
return notValid;
}
})
);
|