/*
* Copyright 2010 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 does display the history of a content in a timeline
* @private
*/
Ext.define('Ametys.plugins.cms.content.tool.HistoryTool', {
extend: "Ametys.tool.SelectionTool",
/**
* @private
* @readonly
* @property {Number} MIN_WIDTH The min width
*/
MIN_WIDTH: 350,
/**
* @private
* @property {Ext.Template} _contentHintTpl The template used for hint description
*/
_contentHintTpl: new Ext.XTemplate("{{i18n PLUGINS_CMS_UITOOL_HISTORY_CONTENT_HINT}} <strong>{[Ext.String.escapeHtml(values.title)]}</strong>"),
/**
* @private
* @property {Ext.Template} _versionTpl The template used for versions
*/
_versionTpl: Ext.create ('Ext.XTemplate', '<tpl for="."><div class="version {cls}" data-record-first="{first}" data-record-last="{last}">{version}</div></tpl>'),
/**
* @private
* @property {List} _workflow The list of versions of the workflow
*/
_workflow: [],
/**
* @cfg {String} showJCRVersionButtonIconCls The separated CSS classes to apply to button to show JCR versions
*/
showJCRVersionButtonIconCls: 'ametysicon-code-css-border-radius decorator-ametysicon-body-part-eye',
/**
* @cfg {String} showJCRVersionButtonTooltip Tooltip of the button to show JCR versions
*/
showJCRVersionButtonTooltip: "{{i18n PLUGINS_CMS_UITOOL_HISTORY_SHOW_JCR_VERSIONS_BUTTON}}",
/**
* @cfg {String} showOnlyWorkflowVersionsButtonIconCls The separated CSS classes to apply to button to hide JCR Versions
*/
showOnlyWorkflowVersionsButtonIconCls: 'ametysicon-code-css-border-radius decorator-ametysicon-body-part-eye-no',
/**
* @cfg {String} showOnlyWorkflowVersionsButtonTooltip Tooltip of the button to hide JCR versions
*/
showOnlyWorkflowVersionsButtonTooltip: "{{i18n PLUGINS_CMS_UITOOL_HISTORY_HIDE_JCR_VERSIONS_BUTTON}}",
constructor: function(config)
{
this.callParent(arguments);
Ametys.message.MessageBus.on(Ametys.message.Message.WORKFLOW_CHANGED, this._onWorkflowChanged, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onDeleted, this);
},
createPanel: function()
{
var me = this;
this._button = Ext.create("Ext.Button", {
// show JCR versions
iconCls: this.showJCRVersionButtonIconCls,
tooltip: this.showJCRVersionButtonTooltip,
scope: this,
enableToggle: true,
toggleHandler: function(btn, state) {
// The starting applyState triggers this function... too soon
if (me._button)
{
me._showJCRVersions(btn, state);
btn.saveState();
}
},
cls: 'a-btn-light',
stateful: true,
stateId: this.getId() + "$jcrversion",
itemId: 'button-toggle-jcr',
applyState: function(state) {
this.setPressed(state && state.pressed);
},
getState: function() {
return {
pressed: this.pressed
};
}
});
this._timeline = Ext.create('Ametys.timeline.Timeline', {
scrollable: true,
minInsideWidth: this.MIN_WIDTH,
dockedItems: {
dock: 'top',
xtype: 'container',
layout: {
type: 'hbox',
align: 'middle'
},
ui: 'tool-hintmessage',
cls:'hint',
items: [
{
// hint
xtype: 'component',
itemId: 'content-info',
ui: 'tool-hintmessage',
html: '',
flex: 1
},
{
xtype: 'container',
layout: {
type: 'vbox',
align: 'stretch'
},
items: [
{
xtype: 'button',
cls: 'a-btn-light',
iconCls: 'ametysicon-sign-info',
disabled: true,
tooltip: "{{i18n PLUGINS_CMS_UITOOL_HISTORY_CONTENT_HINT_2}}"
},
this._button
]
}
]
},
viewConfig: {
getRowClass: function(record) {
return record.get('live') ? 'row-live' : (record.get('valid') ? 'row-valid' : '');
},
listeners: {
'afterrender': Ext.bind(this._onAfterViewRender, this)
}
},
cls: ['a-timeline', 'uitool-history'],
timelineItemHTML: ['<div class="step-wrap {stepCls} {additionalCls}"><div class="step">{step}</div></div>',
'<div class="timeline-item {additionalCls}">',
'<tpl if="profileImg">',
'<div class="profile-img-wrap" data-qtip="{userTooltip}">',
'<tpl if="profileImg"><img src="{profileImg}" alt=""/></tpl>',
'<tpl if="profileGlyph"><span style="height:46px; width: 46px; font-size: 46px" class="profile-img-glyph {profileGlyph}"></span></tpl>',
'<div>{hour}</div>',
'</div>',
'<tpl else>',
'<div class="profile-hour-wrap">',
'<div>{hour}</div>',
'</div>',
'</tpl>',
'<div class="version-line"></div>',
'<tpl if="profileImg">',
'<div class="contents-wrap">',
'<span class="vertical-line"></span>',
'<tpl if="topText && topText != \'\'">',
'<div class="top">{topText}</div>',
'</tpl>',
'<div class="text" data-qtip="{userTooltip}">{text}</div>',
'<tpl if="comment && comment != \'\'">',
'<div class="comment"><span class="x-fa fa-quote-left"></span>{comment}</div>',
'</tpl>',
'<div class="bg-image" style="background-image:url(\'{icon}\')"></div>',
'</div>',
'</tpl>',
'</div>'],
listeners: {
'afterlayout': Ext.bind(this._onAfterLayout, this),
}
});
this._timeline.getStore().on('datachanged', this._onLoad, this);
return Ext.create('Ext.panel.Panel', {
scrollable: false,
border: false,
layout: 'card',
activeItem: 0,
items: [{
xtype: 'component',
cls: 'a-panel-text-empty',
border: false,
html: ''
},
this._timeline
]
});
},
/**
* @private
* Toggle handle of the show JCR versions button
* @param {Object} btn the button
* @param {Object} state the new state of the button
*/
_showJCRVersions: function(btn, state)
{
if (state)
{
btn.setIconCls(this.showOnlyWorkflowVersionsButtonIconCls);
btn.setTooltip(this.showOnlyWorkflowVersionsButtonTooltip)
}
else
{
btn.setIconCls(this.showJCRVersionButtonIconCls);
btn.setTooltip(this.showJCRVersionButtonTooltip);
}
// Update the versions displayed, display or not the intermediate JCR versions
this._updateHistoryView(this._workflow);
},
_isShowJCRVersionPressed: function()
{
return this._button.pressed;
},
refresh: function (manual)
{
this.showRefreshing();
this._workflow = [];
var contentTarget = this.getCurrentSelectionTargets().length > 0 ? this.getCurrentSelectionTargets()[0] : null;
if (contentTarget != null)
{
this._contentId = contentTarget.getParameters().id;
this.getContentPanel().down("#content-info").update(this._contentHintTpl.applyTemplate({'title': contentTarget.getParameters().title}));
Ametys.data.ServerComm.send({
plugin: this.getPluginName(),
url: 'history.json',
parameters: {contentId: this._contentId},
priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
callback: {
handler: this._updateHistoryViewWithServerResponse,
scope: this,
arguments: [this._contentId]
},
responseType: 'text'
});
}
else
{
// Nothing to do.
// Do not use the noSelectionMatch method to avoid erasing the current message
this.showRefreshed();
}
},
/**
* @private
* Callback function to update the history view with the server response
* @param {Object} response the server response
* @param {Object} args the callback arguments
*/
_updateHistoryViewWithServerResponse : function (response, args)
{
var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
if (result != null && result.hasRight == false)
{
this._workflow = [];
var panel = this.getContentPanel().items.get(0);
panel.update(this.getInitialConfig("selection-description-noright"));
this.getContentPanel().getLayout().setActiveItem(0);
this.showRefreshed();
}
else if (result != null)
{
this._workflow = result.workflow;
this._updateHistoryView(this._workflow);
}
else
{
this._workflow = [];
this.getContentPanel().getLayout().setActiveItem(0);
this.showRefreshed();
}
},
/**
* @private
* Check if two dates of format ''2023-10-17T10:49:23.098Z' are the same, ignoring the seconds
* @param {String} stringDate1 the first date
* @param {String} stringDate2 the second date
* @return {String} true if the date are the same (at about 60 seconds), false otherwise
*/
_sameDate(stringDate1, stringDate2)
{
var date1 = new Date(stringDate1);
var date2 = new Date(stringDate2);
return (new Date(date1).getTime() - new Date(date2).getTime()) < 60000;
},
/**
* @private
* Callback function to update the history view from the workflow
* @param {Object} workflow the workflow
*/
_updateHistoryView : function (workflow)
{
// Workflow step are order from newest to oldest
var me = this;
var data = [];
// save the last handled version to be able to determine when we change version
var precedeVersion = null;
var beyondIncompatibleVersion = false;
for (var i=0; i < workflow.length; i++)
{
var step = workflow[i];
var versionIndex = 0;
// All step version except the first one are newer than the step itself
// So we start by handling them.
if (me._isShowJCRVersionPressed() && step.versions.length > 1)
{
// Ignore the oldest version of the step, it will be handled by the step itself
for (; versionIndex < step.versions.length - 1; versionIndex++)
{
var version = step.versions[versionIndex];
var currentVersion = version.name;
var nextVersion = versionIndex < step.versions.length - 1 ? step.versions[versionIndex+1].name : null;
var isFirstOfSameVersion = nextVersion != currentVersion;
var isLastOfSameVersion = precedeVersion != currentVersion;
precedeVersion = currentVersion;
// The step is the current step if the parent step is current and we are dealing with the first element of the list (= the last version)
var d = this._convertSubStep2Timeline(version, (step.current && versionIndex == 0), isLastOfSameVersion, isFirstOfSameVersion, isLastOfSameVersion, beyondIncompatibleVersion);
data.push(d);
beyondIncompatibleVersion = d.beyondIncompatibleVersion;
}
}
// Handled the step version using the next version index for version
// ie newest when hidding JCR version or oldest when displaying the JCR version
var currentVersion = step.versions[versionIndex].name;
// Next version to consider is always in the next step
var nextVersion = i < workflow.length - 1 ? workflow[i+1].versions[0].name : null;
var isFirstOfSameVersion = nextVersion != currentVersion;
var isLastOfSameVersion = precedeVersion != currentVersion;
precedeVersion = currentVersion;
var d = me._convertStep2Timeline(step, i < workflow.length - 1 ? workflow[i+1] : null, isLastOfSameVersion, isFirstOfSameVersion, isLastOfSameVersion, beyondIncompatibleVersion);
data.push(d);
beyondIncompatibleVersion = d.beyondIncompatibleVersion;
}
this._timeline.getStore().loadData(data);
this.getContentPanel().getLayout().setActiveItem(1);
this.showRefreshed();
},
/**
* @private
* Get the label of the version
* @param {Ext.view.Table} view the view
*/
_getVersionLabel: function (showVersion, isCurrent, beyondIncompatibleVersion, versionToUse)
{
var version = null;
if (showVersion)
{
var versionName = versionToUse.name;
var versionLabel = "{{i18n PLUGINS_CMS_UITOOL_HISTORY_VERSION_NAME}}" + (versionName.length > 3 ? '<br/>' : ' ') + versionName;
if (!isCurrent)
{
var rawName = versionToUse.rawName;
if (!rawName)
{
version = "<span class='not-compatible' title=\"{{i18n PLUGINS_CMS_UITOOL_HISTORY_UNAVAILABLE_VERSION}}\">"
+ versionLabel
+ '</span>';
}
else if (!beyondIncompatibleVersion)
{
version = '<a href="javascript:(function(){Ametys.tool.ToolsManager.getTool(\'' + this.getId() + '\').openRevision(\'' + versionName + '\', \'' + rawName + '\')})()">'
+ versionLabel
+ '</a>';
}
else
{
version = "<span class='not-compatible' title=\"{{i18n PLUGINS_CMS_UITOOL_HISTORY_NOTCOMPATIBLE_VERSION}}\">"
+ versionLabel
+ '</span>';
}
}
else
{
version = "<span>" + versionLabel + "<br/>{{i18n PLUGINS_CMS_UITOOL_HISTORY_CURRENT_VERSION}}</span>";
}
}
return version;
},
/**
* @private
* Convert a non workflow step record to a timeline record
* @param {Object} step the step
* @param {boolean} current true if the step is the current one, false otherwise
* @param {Boolean} showVersion should show version
* @param {Boolean} isFirstOfSameVersion true if this step is the first of the current version
* @param {Boolean} isLastOfSameVersion true if this step is the last of the current version
* @param {Boolean} beyondIncompatibleVersion true if we are in the incompatible versions
* @return {Object} the configuration of a timeline record
*/
_convertSubStep2Timeline: function (version, current, showVersion, isFirstOfSameVersion, isLastOfSameVersion, beyondIncompatibleVersion)
{
var additionalCls = [];
var isLive = Ext.Array.contains (version.label, 'Live');
var isVersionValid = version.valid;
beyondIncompatibleVersion = beyondIncompatibleVersion || Ext.Array.contains (version.label, 'NotCompatible'); // At this time only check the most recent version
if (current)
{
additionalCls.push('current');
}
if (isVersionValid)
{
additionalCls.push('valid');
}
if (isLive)
{
additionalCls.push('live');
}
if (isFirstOfSameVersion)
{
additionalCls.push('first');
}
var versionLabel = this._getVersionLabel(showVersion, current, beyondIncompatibleVersion, version);
return {
id: this.getId() + '-' + Ext.id(),
date: version.createdAt,
icon: Ametys.CONTEXT_PATH + version.actionIconMedium,
stepCls: isLastOfSameVersion ? 'last' : '',
additionalCls: additionalCls.join(' '),
versionName: version.name,
version: versionLabel,
live: isLive,
valid: isVersionValid,
beyondIncompatibleVersion: beyondIncompatibleVersion || (version.label == 'NotCompatible'), // For the followers, we need to check the tag in all versions
topText: '',
cls: 'step'
}
},
/**
* @private
* Compute the tooltip
* @param {Object} user The user to display
* @returns {String} The tooltip
*/
_getUserTooltip: function(user)
{
var login = user.login;
var populationId = user.populationId;
var username = user.fullname || "{{i18n plugin.core:PLUGINS_CORE_USERS_UNKNOWN_USER}}";
var populationLabel = user.populationLabel || populationId;
return Ametys.helper.Users.renderUser(login, populationLabel, username);
},
/**
* @private
* Convert a workflow step record to a timeline record
* @param {Object} step the workflow step
* @param {Boolean} showVersion should show version
* @param {Boolean} isFirstOfSameVersion true if this step is the first of the current version
* @param {Boolean} isLastOfSameVersion true if this step is the last of the current version
* @param {Boolean} beyondIncompatibleVersion true if we are in the incompatible versions
* @return {Object} the configuration of a timeline record
*/
_convertStep2Timeline: function (step, previousStep, showVersion, isFirstOfSameVersion, isLastOfSameVersion, beyondIncompatibleVersion)
{
function _hasTag(versions, tag)
{
for (var i = 0; i < versions.length; i++)
{
if (versions[i].label == tag)
{
return true;
}
}
return false;
}
var additionalCls = [];
var isValid = step.validation;
var isVersionValid = step.versions[0].valid;
beyondIncompatibleVersion = beyondIncompatibleVersion || Ext.Array.contains (step.versions[0].label, 'NotCompatible'); // At this time only check the most recent version
var versions = step.versions;
var versionToUse = versions[0];
if (this._isShowJCRVersionPressed() && versions.length > 1)
{
versionToUse = versions[versions.length - 1];
if (!this._sameDate(versionToUse.createdAt, step.date))
{
if (previousStep != null)
{
versionToUse = previousStep.versions[0];
}
}
}
var isLive = Ext.Array.contains (versionToUse.label, 'Live');
var isCurrent = versionToUse == versions[0] ? step.current : false;
if (isCurrent)
{
additionalCls.push('current');
}
if (isValid || isVersionValid)
{
additionalCls.push('valid');
}
if (isLive)
{
additionalCls.push('live');
}
if (isFirstOfSameVersion)
{
additionalCls.push('first');
}
var version = this._getVersionLabel(showVersion, isCurrent, beyondIncompatibleVersion, versionToUse);
return {
id: this.getId() + '-' + Ext.id(),
date: step.date,
username: (step.caller ? step.caller.fullname : '') || "{{i18n plugin.core:PLUGINS_CORE_USERS_UNKNOWN_USER}}",
userTooltip: step.caller ? this._getUserTooltip(step.caller) : null,
profileImg: Ametys.helper.Users.getUserImage(step.caller ? step.caller.login : 'undefined', step.caller ? step.caller.populationId : 'undefined', 46),
icon: Ametys.CONTEXT_PATH + step.actionIconMedium,
text: step.actionLabel,
step: step.description,
stepCls: isLastOfSameVersion ? 'last' : '',
comment: step.comment,
additionalCls: additionalCls.join(' '),
versionName: versionToUse.name,
version: version,
live: isLive,
valid: isValid || isVersionValid,
beyondIncompatibleVersion: beyondIncompatibleVersion || _hasTag(step.versions, 'NotCompatible'), // For the followers, we need to check the tag in all versions
topText: '',
cls: 'step'
}
},
/**
* @private
* Listener when the view is rendered.
* Insert a container element for versions
* @param {Ext.view.Table} view the view
*/
_onAfterViewRender: function (view)
{
// 1 - Insert container for versions
view.getEl().insertFirst({
tag: 'div',
style: 'min-width: ' + this.MIN_WIDTH + 'px',
cls: 'versions-container'
});
view.mon(view.getEl(), 'scroll', Ext.bind(this._onScroll, this));
},
/**
* @private
* Listener when the store is loaded
* @param {Ametys.timeline.Timeline.TimelineStore} store The store
*/
_onLoad: function (store)
{
// 2 - Insert versions labels in container
var versions = [];
var currentVersion = null;
var beginRecord = null;
var lastRecord = null;
var record1, record2;
store.getData().each (function (record) {
var v = record.get('versionName');
if (v != currentVersion)
{
if (currentVersion != null)
{
versions.push ({
version: beginRecord.get('version'),
first: beginRecord.getId(),
last: lastRecord.getId(),
cls: beginRecord.get('additionalCls')
});
}
currentVersion = v;
beginRecord = record;
}
lastRecord = record;
});
versions.push ({
version: beginRecord.get('version'),
first: beginRecord.getId(),
last: lastRecord.getId(),
cls: beginRecord.get('additionalCls')
});
this._timeline.getView().getEl().down('.versions-container').update('');
var html = this._versionTpl.apply(versions);
this._timeline.getView().getEl().down('.versions-container').update(html);
},
/**
* @private
* Positions the versions in right place
*/
_positionVersions: function (force)
{
// 3 - Position versions in right place
var vEl = this._timeline.getView().getEl().down('.versions-container').down('.version');
var scrollTop = this._timeline.getView().getEl().getScrollTop();
while (vEl != null)
{
if (!vEl.alreadySet || force)
{
var firstId = vEl.getAttribute("data-record-first");
var firstRecord = this._timeline.getStore().getById(firstId);
var firstRow = this._timeline.getView().getRow(firstRecord);
if (firstRow) // firstRow may not exists since the render can be delayed when there are too many elements
{
var lastId = vEl.getAttribute("data-record-last");
var lastRecord = this._timeline.getStore().getById(lastId);
var lastRow = this._timeline.getView().getRow(lastRecord);
var top1 = Ext.get(firstRow).down('.timeline-item').getOffsetsTo(this._timeline.getView().getEl())[1];
var top2 = Ext.get(lastRow).getOffsetsTo(this._timeline.getView().getEl())[1] + Ext.get(lastRow).getHeight() - 5;
vEl.show();
vEl.removeCls('rotate'); // remove rotate transformation before calculating
vEl.setTop(scrollTop + top1 + (top2 - top1 - vEl.getHeight())/2);
vEl.setRight(25 - vEl.getWidth()/2);
vEl.addCls('rotate');
vEl.alreadySet = true;
}
else
{
vEl.hide();
}
}
vEl = vEl.next();
}
},
/**
* @private
* Set the css classes for headers
*/
_setGroupHeaderCls: function ()
{
var tableEl = this._timeline.getView().getEl().down('table').next(); // ignore first header group
while (tableEl != null)
{
var hdEl = tableEl.down('.x-group-hd-container');
if (hdEl != null)
{
var prevTableEl = tableEl.prev();
if (prevTableEl.down('.timeline-item.first') != null)
{
// It is not the same version, cut the line of versions
hdEl.addCls('hd-blank');
}
else
{
var rowEl = tableEl.down('.x-grid-row');
var cls = rowEl.getAttribute('class');
if (cls.indexOf('row-live') != -1)
{
hdEl.addCls('hd-live');
}
else if (cls.indexOf('row-valid') != -1)
{
hdEl.addCls('hd-valid');
}
}
}
tableEl = tableEl.next();
}
},
/**
* @private
* Listener after the layout of the timeline
*/
_onAfterLayout: function ()
{
this._positionVersions(true);
this._setGroupHeaderCls();
},
/**
* @private
* Listener after the scroll of the timeline
*/
_onScroll: function ()
{
// Some versions may not have been positioned yet
this._positionVersions();
},
/**
* Open a content revision
* @param versionName The version name of old revision
* @param contentVersion The content version
*/
openRevision: function (versionName, contentVersion)
{
Ametys.tool.ToolsManager.openTool("uitool-old-content", {id: this._contentId, versionName: versionName, contentVersion: contentVersion});
},
setNoSelectionMatchState: function (message)
{
this.callParent(arguments);
var panel = this.getContentPanel().items.get(0);
panel.update(message);
this.getContentPanel().getLayout().setActiveItem(0);
this._contentId = null;
this._workflow = [];
},
/**
* Listener on {@link Ametys.message.Message#WORKFLOW_CHANGED} message. If the current
* content is concerned, the tool will be out-of-date.
*
* @param {Ametys.message.Message} message The workflow changed message.
* @protected
*/
_onWorkflowChanged: function (message)
{
if (this.getTargetsInCurrentSelectionTargets(message).length > 0)
{
this.showOutOfDate();
}
},
/**
* Listener on {@link Ametys.message.Message#DELETED} message. If the current content is concerned, the tool will be set in no selection mode.
* @param {Ametys.message.Message} message The deleted message.
* @protected
*/
_onDeleted: function (message)
{
if (this.getTargetsInCurrentSelectionTargets(message).length > 0)
{
this.setNoSelectionMatchState();
}
},
_getMatchingSelectionTargets: function(message)
{
var targets = this.callParent(arguments);
var trueTargets = []
// Let's check if the content is WorkflowAware
for (var i = 0; i < targets.length; i++)
{
var target = targets[i];
if (target.getParameters()['workflowName'])
{
trueTargets.push(target);
}
}
return trueTargets;
}
});