/*
* Copyright 2020 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This tool shows the difference between two versions of the same content.
*/
Ext.define('Ametys.plugins.cms.content.compare.CompareContentVersionTool', {
extend: 'Ametys.tool.Tool',
toolCls: ['contenttool', 'contenttoolcomparator'],
/**
* @cfg {String} showEmptyDataButtonIconCls The separated CSS classes to apply to button to show empty data fields
*/
showAllDataButtonIconCls: 'ametysicon-text70 decorator-ametysicon-body-part-eye',
/**
* @cfg {String} showEmptyDataButtonTooltip Tooltip of the button to show empty data fields
*/
showAllDataButtonTooltip: "{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_DATA_ALL}}",
/**
* @cfg {String} hideEmptyDataButtonIconCls The separated CSS classes to apply to button to hide empty data fields
*/
showDiffDataButtonIconCls: 'ametysicon-text70 decorator-ametysicon-body-part-eye-no',
/**
* @cfg {String} showEmptyDataButtonTooltip Tooltip of the button to hide empty data fields
*/
showDiffDataButtonTooltip: "{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_DATA_ONLYDIFF}}",
/**
* @property {Boolean} _showAllData Show all fields? or only modified fields...
* @private
*/
_showAllData: true,
/**
* @private
* @property {String} _contentId The content id
*/
/**
* @private
* @property {String} _baseVersion The base content version
*/
/**
* @private
* @property {String} _version The content version to be compared
*/
/**
* @private
* @property {Ametys.plugins.cms.content.compare.CompareContentVersionPanel} _diffPanel The panel showing the diff between the two versions of the content
*/
constructor: function(config)
{
this.callParent(arguments);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
Ametys.message.MessageBus.on(Ametys.message.Message.WORKFLOW_CHANGED, this._onMessageModified, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onDeleted, this);
},
setParams: function(params)
{
this._openedAtStartup = !Ametys.tool.ToolsManager.isInitialized();
this._contentId = params.contentId;
this._baseVersion = params.baseVersion;
this._version = params.version;
this._onLoadHistory();
this._viewName = params['view-name'] || 'default-edition';
this._fallbackViewName= params['fallback-view-name'] || 'main';
this._onLoadViews();
if (params.allData !== undefined)
{
this._showAllData = !params.allData; // Will be reverted by the toggle under
this.getWrapper().down("#alldatabtn").toggle(!params.allData);
}
this.callParent(arguments);
this.showOutOfDate();
this._updateInfos();
},
/**
* Update tool information
* @private
*/
_updateInfos: function()
{
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.repository.ContentDAO",
methodName: 'getContentDescription',
parameters: [ this.getContentId(), this._workspaceName],
waitMessage: false,
cancelCode: 'CompareContentVersionTool$updateInfo$' + this.getContentId(),
callback: {
handler: this._getContentDescriptionCb,
scope: this,
ignoreOnError: false
}
});
},
/**
* Callback function called after #_updateInfos is processed
* @param {Object} data The server response
* @param {String} data.title The title
* @param {Object} data.lastContributor The last contributor object
* @param {String} data.lastContributor.fullname The fullname of the last contributor
* @param {String} data.lastModified The last modified date at 'd/m/Y, H:i' format
* @param {String} data.iconGlyph A css class to set a glyph
* @param {String} data.iconDecorator A css class to set a glyph decorator
* @param {String} data.smallIcon The path to the small (16x16) icon. iconGlyph win if specified
* @param {String} data.mediumIcon The path to the medium (32x32) icon. iconGlyph win if specified
* @param {String} data.largeIcon The path to the large (48x48) icon. iconGlyph win if specified
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_getContentDescriptionCb: function (data, args)
{
if (!this.isNotDestroyed())
{
return;
}
if (!data)
{
// Alert user then close tool
var details = '';
var params = this.getParams();
for (var name in params)
{
details += name + " : " + params[name] + '\n';
}
if (!this._openedAtStartup)
{
Ametys.log.ErrorDialog.display({
title: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_TITLE}}",
text: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_CLOSE_MSG}}",
details: details,
category: this.self.getName()
});
}
else
{
Ametys.notify({
title: "{{i18n PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_TITLE}}",
description: "{{i18n PLUGINS_CMS_TOOL_CONTENT_NOT_FOUND_CLOSE_MSG}}",
type: "warn"
});
this.getLogger().error("Cannot open unexisting content");
}
this.close();
return;
}
this._openedAtStartup = false;
this._updateTitleAndTooltip(data.title);
/*if (data.iconGlyph)
{
this.setGlyphIcon(data.iconGlyph);
this.setIconDecorator(data.iconDecorator);
}
else
{
this.setGlyphIcon(null);
this.setSmallIcon(data.smallIcon);
this.setMediumIcon(data.mediumIcon);
this.setLargeIcon(data.largeIcon);
}*/
},
/**
* @private
* Update the title/tooltip with versions
* @param {String} [newTitle] If the title has changed
*/
_updateTitleAndTooltip: function(newTitle)
{
newTitle = newTitle || this._rawTitle || "";
this._rawTitle = newTitle;
var toolTitle = this.getInitialConfig("dynamic-title");
var toolDescription = this.getInitialConfig("dynamic-description");
var versionIndex = this._versionsStore.find('value', this._version);
var version = versionIndex == -1 ? '?' : this._versionsStore.getAt(versionIndex).get('label');
var baseVersionIndex = this._versionsStore.find('value', this._baseVersion);
var baseVersion = baseVersionIndex == -1 ? '?' : this._versionsStore.getAt(baseVersionIndex).get('label');
this.setTitle(Ext.String.format(toolTitle, newTitle, baseVersion, version));
this.setDescription(Ext.String.format(toolDescription, newTitle, baseVersion, version));
},
/**
* @protected
* Get the top panel for hint and actions
* @return {Ext.Panel} the top panel
*/
_getTopPanel: function ()
{
let rightPartWidth = 230;
return Ext.create({
dock: 'top',
xtype: 'container',
layout: {
type: 'hbox',
align: 'middle'
},
cls: 'top',
items: [
{
// hint
xtype: 'component',
itemId: 'hint',
cls: 'hint',
html: '<div style="text-align: right">{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_HINT_1}} </div>',
width: Ametys.form.ConfigurableFormPanel.LABEL_WIDTH + rightPartWidth - 80 /* right margin */
},
this._createHistoriesComboBox(),
{
xtype: 'container',
layout: {
type: 'hbox',
align: 'middle'
},
width: rightPartWidth,
items: [
this._createViewsComboBox(),
{
// show/hide empty data
xtype: 'button',
itemId: 'alldatabtn',
iconCls: this._allDataIconCls(),
tooltip: this._allDataTooltip(),
scope: this,
enableToggle: true,
toggleHandler: this.toggleAllData,
cls: 'a-btn-light'
}
]
},
]
});
},
_allDataIconCls: function()
{
return !this._showAllData ? this.showAllDataButtonIconCls : this.showDiffDataButtonIconCls;
},
_allDataTooltip: function()
{
return !this._showAllData ? this.showAllDataButtonTooltip : this.showDiffDataButtonTooltip;
},
/**
* Show/hide all data
* @param {Ext.button.Button} button The button which triggered this method. Parameter not used.
* @param {Boolean} state If true, show empty data
* @private
*/
toggleAllData: function(btn, state)
{
this._showAllData = !this._showAllData;
btn.setIconCls(this._allDataIconCls());
btn.setTooltip(this._allDataTooltip())
this.showOutOfDate();
},
/**
* Create a combo box for the views
* @private
*/
_createHistoriesComboBox: function()
{
this._versionsStore = Ext.create('Ext.data.Store', {
model: 'Ametys.plugins.cms.content.compare.CompareContentVersionTool.CompareContentVersion',
proxy: {
type: 'ametys',
plugin: 'cms',
url: 'version-history.json',
reader: {
type: 'json'
}
},
autoLoad: true,
listeners: {
load: {fn: this._onLoadHistory, scope: this},
beforeload: {fn: this._onBeforeLoadHistory, scope: this}
}
});
var cfg = {
cls: 'ametys',
forceSelection: true,
editable: false,
queryMode: 'local',
allowBlank: false,
store: this._versionsStore,
hidden: false,
valueField: 'value',
displayField: 'fullLabel',
flex: 1,
listeners: {
change: {fn: this._onChangeVersion, scope: this}
},
tpl: new Ext.XTemplate([
'{% this.currentGroup = null; %}',
'<div class="x-list-plain contenttoolcomparator-version-list">',
'<tpl for=".">',
'<tpl for="day" if="this.shouldShowHeader(values.day)"><div class="contenttoolcomparator-version-list-category">{[this.showHeader(values.day)]}</div></tpl>',
'<div class="x-boundlist-item contenttoolcomparator-version-list-item">{listLabel}</div>',
'</tpl>',
'</div>',
{
shouldShowHeader: function(group){
return this.currentGroup != group;
},
showHeader: function(group){
this.currentGroup = group;
return group;
}
}
])
}
this._versions1combo = Ext.create('Ext.form.field.ComboBox', cfg);
this._versions2combo = Ext.create('Ext.form.field.ComboBox', cfg);
return {
xtype: 'container',
layout: {
type: 'hbox',
align: 'middle'
},
flex: 1,
items: [
this._versions1combo,
{
xtype: 'component',
html: " {{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_HINT_2}} "
},
this._versions2combo
]
};
},
/**
* Create a combo box for the views
* @private
*/
_createViewsComboBox: function()
{
this._viewStore = Ext.create('Ext.data.Store', {
proxy: {
type: 'ametys',
role: 'org.ametys.cms.repository.ContentDAO',
methodName: 'getContentViews',
methodArguments: ['contentId','includeInternal'],
reader: {
type: 'json'
}
},
autoLoad: true,
sortOnLoad: true,
sorters: [{property: 'label', direction:'ASC'}],
listeners: {
load: {fn: this._onLoadViews, scope: this},
beforeload: {fn: this._onBeforeLoadViews, scope: this}
}
});
this._combo = Ext.create('Ext.form.field.ComboBox', {
cls: 'ametys',
forceSelection: true,
editable: false,
queryMode: 'local',
allowBlank: false,
store: this._viewStore,
hidden: false,
valueField: 'name',
displayField: 'label',
labelSeparator: '',
fieldLabel:"{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_HINT_3}} ",
width: 200,
labelWidth: 50,
labelAlign: "right",
listeners: {
change: {fn: this._onChangeView, scope: this}
}
});
return this._combo;
},
/**
* Listener called before load form display view combo box
* @param {Object} store The store
* @param {Object[]} operation The operation
* @private
*/
_onBeforeLoadHistory: function(store, operation)
{
operation.setParams( Ext.apply(operation.getParams() || {}, {
contentId: this._contentId
}));
},
/**
* Listener called before load form display view combo box
* @param {Object} store The store
* @param {Object[]} operation The operation
* @private
*/
_onBeforeLoadViews: function(store, operation)
{
operation.setParams( Ext.apply(operation.getParams() || {}, {
contentId: this._contentId,
includeInternal: true
}));
},
/**
* Listener called after load form display view combo box
* @param {Object} store The store
* @param {Object[]} data The data
* @private
*/
_onLoadViews: function(store, data)
{
if (this._combo.getStore().find('name', this._viewName) != -1)
{
this._combo.setValue(this._viewName);
}
else
{
this._combo.setValue(this._fallbackViewName);
}
},
/**
* Listener called after load form display view combo box
* @param {Object} store The store
* @param {Object[]} data The data
* @private
*/
_onLoadHistory: function(store, data)
{
let version = this._version; // Remember value, since changing the baseVersion will replace the _version variable
if (this._versions1combo.getStore().find('value', this._baseVersion) != -1)
{
this._versions1combo.setValue(this._baseVersion);
}
if (this._versions2combo.getStore().find('value', version) != -1)
{
this._versions2combo.setValue(version);
}
},
/**
* Listener called on change of the view combo box
* @param {Object} combo The combo box
* @param {String} newValue The new value
* @param {String} oldValue The old value
* @private
*/
_onChangeView: function(combo, newValue, oldValue)
{
if (this._viewName != newValue)
{
this._viewName = newValue;
this._updateParams();
this.showOutOfDate();
}
},
/**
* Listener called on change of the view combo box
* @param {Object} combo The combo box
* @param {String} newValue The new value
* @param {String} oldValue The old value
* @private
*/
_onChangeVersion: function(combo, newValue, oldValue)
{
var oldBase = this._baseVersion;
var oldVersion = this._version;
this._baseVersion = this._versions1combo.getValue() || this._baseVersion;
this._version = this._versions2combo.getValue() || this._version;
if (this._baseVersion != oldBase || this._version != oldVersion)
{
this._updateParams();
this._updateTitleAndTooltip();
this.showOutOfDate();
}
},
/**
* @private
*/
_updateParams: function()
{
var newParams = Ext.apply(this.getParams(), {
"baseVersion": this._baseVersion,
"version": this._version,
"view-name": this._viewName,
});
Ametys.plugins.cms.content.compare.CompareContentVersionTool.superclass.setParams.call(this, newParams);
},
createPanel: function()
{
this.form = this._createFormPanel();
Ext.override(this.form, {
// override _addRepeater to force readOnly mode
_addRepeater: function (ct, config, initialSize)
{
config.readOnly = true;
this.callParent(arguments);
}
});
return Ext.create('Ext.Panel', {
cls: this.toolCls,
layout: 'fit',
dockedItems: this._getTopPanel(),
items: [
this.form
]
});
},
_createFormPanel: function()
{
return Ext.create('Ametys.form.ConfigurableFormPanel', {
itemId: 'form-panel',
cls: 'content-form-inner',
scrollable: true,
additionalWidgetsConfFromParams: {
contentType: 'contentType' // some widgets require the contentType configuration
},
fieldNamePrefix: '',
displayGroupsDescriptions: false,
baseMode: null // FIXME Hack to be able to set values for reference (base values), then for values to be compared (use by Ametys.form.widget.Comparator)
});
},
refresh: function()
{
this.showRefreshing();
this.form.destroyComponents();
this.form.baseMode = null;
// Get the comparison result
this.serverCall("getDiffValues", [this._contentId, this._viewName, this._showAllData, this._version, this._baseVersion], this._drawForm, {refreshing: true, cancelCode: 'CompareContentVersionTool$refresh$' + this.getContentId(),});
this._updateInfos();
},
/**
* Callback function called after retrieving different values from the content.
* This draws the form for content edition
* @param {Object} response The XML response provided by the {@link Ametys.data.ServerComm}
* @param {Object} args The callback parameters passed to the {@link Ametys.data.ServerComm#send} method
* @private
*/
_drawForm: function (response, args)
{
// Create form from view
var viewItems = response.view.elements;
if (Object.keys(viewItems).length == 0)
{
this.form.mask("{{i18n PLUGINS_CMS_COMPARE_CONTENT_VERSIONS_TOOL_NODATA}}", "ametys-mask-unloading")
}
else
{
this.form.unmask();
}
// Could not be set during the createPanel since, the _contentId is not set yet
this.form.setAdditionalWidgetsConf("contentInfo", { contentId: this._contentId });
this.form.configure(viewItems);
// Fill form fields
this._fillForm(response);
this.sendCurrentSelection();
},
_fillForm: function(response)
{
var baseValues = this._stringToXml(response.baseValues);
var values = this._stringToXml(response.values);
// FIXME REPOSITORY-454 For now we cannot have values to JSON format
// If that is the case, we can joined the JSON values (reference and current) and call this.form.setValues once.
// Here we have to #setValues twice : for reference values and for current values, which is really bad
// When it will be fixed, baseMode boolean could be removed.
// Switch to reference mode to set reference values
this.form.baseMode = true;
this.form.setValues(baseValues, "metadata");
this._overrideFormAfterBaseValuesAreSet();
// Switch to current mode to set current values
this.form.baseMode = false;
this.form.setValues(values, "metadata");
// Set the comparison state
this._setDiffState(response.changedAttributeDataPaths);
// Expand repeaters
this._expandRepeaterEntries();
this.showRefreshed();
},
/**
* @private
*/
_overrideFormAfterBaseValuesAreSet: function()
{
var allRepeaters = this.form.query("panel[isRepeater]");
Ext.Array.forEach(allRepeaters, function(repeater) {
Ext.override(repeater, {
// the #setValues > ... > #_setValuesXMLMetadata > repeaterPanel#removeItem
// can be called
// so we override here to prevent any removing of any repeater entry
removeItem: function(itemPanel)
{
// Do not call parent, prevent any removing !
// there is a target repeater entry which is not present on current values => it was removed
}
});
});
},
/**
* @private
* Set the diff state for each comparison fields
* @param {Object} changedAttributeDataPaths The path of attributes with changes
*/
_setDiffState: function(changedAttributeDataPaths)
{
var me = this;
function _hasDiff(name)
{
return Ext.Array.contains(changedAttributeDataPaths, name) /* trivial case => the current data path is in the changes */
|| _parentRepeaterOrCompositeHasChanged(name);
}
function _parentRepeaterOrCompositeHasChanged(name)
{
// case where changedAttributeDataPaths contains 'rep[2]' for instance, and the current data path is 'rep[2]/someStringAttribute'
// => it should be marked with a diff too
return changedAttributeDataPaths
.filter(function(dataPath) {
return Ext.String.startsWith(name, dataPath + "/")
|| Ext.String.startsWith(name, dataPath + "[");
})
.length > 0;
}
var comparisonFields = this._getComparisonFields();
Ext.Array.each(comparisonFields, function(field){
var fieldName = field.getName();
if (_hasDiff(fieldName))
{
field.setInDiffState(true);
}
else if (!me._showAllData)
{
field.hide(); // can happen in repeaters
_removeIfNotAnyVisibleChild(field.ownerCt);
{
}
}
});
function _removeIfNotAnyVisibleChild(panel)
{
let oneChildIsVisible = false;
panel.items.each(function(item) {
if (!item.isHidden())
{
oneChildIsVisible = true;
return false;
}
});
if (!oneChildIsVisible)
{
let parent = panel.ownerCt;
panel.ownerCt.remove(panel);
_removeIfNotAnyVisibleChild(parent);
}
}
},
/**
* @private
* Expands all entries of all repeaters
*/
_expandRepeaterEntries: function()
{
this.form.query('panel[isRepeater]').forEach(function(repeater) {
repeater.expandAll();
});
},
/**
* Get the comparison fields
* @return {Ext.form.Field[]} the comparison fields
*/
_getComparisonFields: function()
{
return this.form.getFieldNames()
.map(function(fieldName) {
return this.form.getField(fieldName);
}, this)
.filter(function(field) {
return field.isComparatorField;
});
},
/**
* Get the unique identifier of the content.
* @returns {String} The identifier of the content
*/
getContentId: function()
{
return this._contentId;
},
/**
* Listener on {@link Ametys.message.Message#MODIFIED} message. If the current content is concerned, the tool will be out-of-date.
* @param {Ametys.message.Message} message The message.
* @protected
*/
_onMessageModified: function(message)
{
var targets = message.getTargets(Ametys.message.MessageTarget.CONTENT);
var contentIds = targets.map(function(target) {
return target.getParameters().id;
});
if (Ext.Array.contains(contentIds, this._contentId))
{
this._versions1combo.getStore().reload();
this._versions2combo.getStore().reload();
}
},
/**
* Listener on {@link Ametys.message.Message#DELETED} message. If the current content is concerned, the tool will be closed.
* @param {Ametys.message.Message} message The deleted message.
* @protected
*/
_onDeleted: function (message)
{
var me = this;
var contentTargets = message.getTargets(Ametys.message.MessageTarget.CONTENT);
for (var i=0; i < contentTargets.length; i++)
{
if (this._contentId == contentTargets[i].getParameters().id)
{
this.close();
}
}
},
getMBSelectionInteraction: function()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
getType: function()
{
return Ametys.tool.Tool.TYPE_CONTENT;
},
sendCurrentSelection: function()
{
return Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: {
id: Ametys.message.MessageTarget.CONTENT,
parameters: {
ids: [this._contentId]
}
}
});
},
/**
* @private
* Converts given string into XMLDocument
* @param {String} xmlAsString The XML/HTML as string
* @return {XMLDocument} The XML representation
*/
_stringToXml: function(xmlAsString)
{
var xmlDoc = new DOMParser().parseFromString(xmlAsString, 'text/xml');
return xmlDoc;
}
});