/*
* Copyright 2023 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.
*/
/**
* Mixin for contents grid or contents tree grid
*/
Ext.define('Ametys.cms.content.EditContentsView', {
extends: "Ext.Mixin",
statics: {
/**
* @readonly
* @private
* @property {String} _LOCK_STATE_WORKING Constant for lock state when a lock or a unlock was fired
*/
_LOCK_STATE_WORKING: 'WORKING',
/**
* @readonly
* @private
* @property {String} _LOCK_STATE_IDLE Constant for the lock state when no locking or unlocking process is currently in progress
*/
_LOCK_STATE_IDLE: 'IDLE'
},
/**
* @cfg {Number/String} [workflowEditActionId=2] The workflow action to use when editing the grid
*/
/**
* @cfg {String} [messageTarget=Ametys.message.MessageTarget#CONTENT] The message target factory role to use for the event of the bus thrown and listened by the grid
*/
/**
* @cfg {String} [viewName=default-edition] The name of the view to use when editing the grid
*/
/**
* @cfg {Boolean} [withSaveBar=true] Enable the docked save bar for the grid.
*/
/**
* @property {Boolean} withSaveBar Is the docked save enabled for the grid?
* @readonly
*/
/**
* @cfg {Number} [saveBarPosition] The position of the toolbar between docked items. Default is the last position.
*/
/**
* Map of objects that needs to be locked/unlocked
* Linked to issue CMS-8736 when navigating in the grid faster than the network calls
* @property {Object} _lockedMap map of objects, with the content's id as key
* @property {Object} _lockedMap.CONTENTID information about the lock/unlock status of a content :
* @property {String} _lockedMap.CONTENTID.state state of the lock/unlock ('WORKING' if a lock/unlock process was fired, 'IDLE' otherwise)
* @property {String} _lockedMap.CONTENTID.count count of calls to lock/unlock (+1 for lock, -1 for unlock)
* @property {Object[]} _lockedMap.CONTENTID.actions list of actions needed (can contains more than 1 if more lock/unlock are added during a network call)
* @property {String} _lockedMap.CONTENTID.actions.action "lock" or "unlock" : type of action that added this
@property {Function} _lockedMap.CONTENTID.actions.callback function that will be called after the lock/unlock
* @private
*/
constructor: function(config)
{
this._lockedMap = {};
this.workflowEditActionId = parseInt(config.workflowEditActionId || 2);
this.viewName = config.viewName || "default-edition";
if (config.allowEdition !== false)
{
this.addPlugin(this._createEditPlugin(config));
}
this.withSaveBar = config.withSaveBar !== false; // default to true
if (this.withSaveBar)
{
if (config.saveBarPosition != undefined)
{
this.insertDocked(config.saveBarPosition, this._createSaveBar(config));
}
else
{
this.addDocked(this._createSaveBar(config));
}
}
this.store.on('load', this._onLoad, this);
this.store.on('update', this._onUpdate, this);
this.addCls('edit-contents-view');
},
/**
* @private
* Listener when the store is loaded
*/
_onLoad: function ()
{
if (this.editingPlugin)
{
this._doCancelEdition();
}
},
/**
* @private
* Listener when the store is update to report modification on similar records
*/
_onUpdate: function(store, activeRecord, operation, modifiedFieldNames, details, eOpts)
{
if (!this.changeListeners)
{
this.updateChangeListeners();
}
// Report modification on others records with the same contentid
let contentId = this._getContentId(activeRecord);
let records = this._findRecords(contentId);
for (let record of records)
{
if (record != activeRecord)
{
if (operation == Ext.data.Model.EDIT)
{
for (let fieldName of modifiedFieldNames)
{
if (activeRecord.get(fieldName + "_content") !== undefined)
{
record.set(fieldName + "_content", activeRecord.get(fieldName + "_content"));
}
if (activeRecord.get(fieldName + "_repeater") !== undefined)
{
record.set(fieldName + "_repeater", activeRecord.get(fieldName + "_repeater"));
}
record.set(fieldName, activeRecord.get(fieldName));
}
}
}
}
// after a local load, the state may have changed
this._showHideSaveBar();
if (this.lockedGrid)
{
// avoid non matching columns height
this.lockedGrid.updateLayout()
}
},
/**
* Call this after setting fields
*/
updateChangeListeners: function()
{
// WARNING this function can be called by the SearchGridRepeaterDialog
let me = this;
if (this.store.model.fields)
{
if (this.changeListeners)
{
this.store.un('update', checkChange);
}
this.store.on('update', checkChange);
this.store.checkChangeNow = checkChange;
this.changeListeners = {};
for (let field of this.store.model.fields.filter(f => f.ftype))
{
recursivelyPrepareField(field, '');
}
}
function checkChange(store, record, operation, modifiedFieldNames, details, eOpts)
{
if (me.changeListeners && modifiedFieldNames)
{
for (let modifieldFieldName of modifiedFieldNames)
{
if (me.changeListeners[modifieldFieldName])
{
for (let handler of me.changeListeners[modifieldFieldName])
{
handler({record: record}, record.get(modifieldFieldName), record.getPrevious(modifieldFieldName));
}
}
}
}
}
function recursivelyPrepareField(field, prefix)
{
let changeListeners = field.changeListeners || (field['widget-params'] ? field['widget-params'].changeListeners : null);
if (changeListeners)
{
try
{
changeListeners = JSON.parse(changeListeners);
let f = Ametys.form.WidgetManager.getWidget(field.widget, field.ftype || field.type, {});
let $class = eval(f.$className);
if (!$class.onRelativeValueChange)
{
me.getLogger().error("The widget " + f.$className + " does not support onRelativeValueChange set in its widget-params 'changeListeners'. Listeners will be ignored.");
}
else
{
for (let event of Object.keys(changeListeners))
{
let paths = Ext.Array.from(changeListeners[event]);
for (let path of paths)
{
function proxyHandler(relativePath, data, newValue, oldValue)
{
let handled = $class.onRelativeValueChange(event, relativePath, data, newValue, oldValue);
if (handled === false)
{
me.getLogger().error("The method " + f.$className + "#onRelativeValueChange does not support the event '" + event + " set in the widget-params")
}
}
Ametys.form.Widget.onRelativeValueChange(path, { grid: me, dataPath: prefix + field.name }, proxyHandler, me);
}
}
}
}
catch (e)
{
me.getLogger().error("The value of 'widget-params' for '" + field.name + "' is not a valid json. It will be ignored.", e);
}
}
// Recursive
if (field.subcolumns)
{
for (let subField of field.subcolumns)
{
recursivelyPrepareField(subField, field.name + "/");
}
}
}
},
/**
* @private
* Create the plugin instance to edit the grid
* @param {Object} config The constructor configuration
* @return {Object} The edit plugin
* @fires
*/
_createEditPlugin: function(config)
{
return {
ptype: 'cellediting',
clicksToEdit: 1,
editAfterSelect: true,
moveEditorOnEnter: true,
_getContentId: Ext.bind(this._getContentId, this), // For repeater widget
_findRecords: Ext.bind(this._findRecords, this), // For repeater widget
listeners: {
'beforeedit': Ext.bind(this._beforeEdit, this),
'canceledit': Ext.bind(this._cancelEdit, this),
'edit': Ext.bind(this._edit, this),
'validateedit': Ext.bind(this._validateEdit, this)
},
activateCell: this._activateCell
};
},
/**
* @private
* Override of the plugin cell edition. used also by the repeater grid
* this = the plugin and not the grid.
* This method is called when actionable mode is requested for a cell.
* @param {Ext.grid.CellContext} position The position at which actionable mode was requested.
* @param {Boolean} skipBeforeCheck Pass `true` to skip the possible vetoing conditions
* like event firing.
* @param {Boolean} doFocus Pass `true` to immediately focus the active editor.
* @return {Boolean} `true` if this cell is actionable (editable)
*/
_activateCell: function(position, skipBeforeCheck, doFocus, isResuming)
{
const activated = Ext.grid.plugin.CellEditing.prototype.activateCell.apply(this, arguments);
if (activated)
{
let field = this.getActiveEditor().field;
if (field.switchToValue !== undefined)
{
field.setValue(field.switchToValue);
field.switchToValue = undefined;
}
if (field.changeListeners)
{
let changeListeners = JSON.parse(field.changeListeners);
let $class = eval(field.$className);
if (!$class.onRelativeValueChange)
{
me.getLogger().error("The widget " + field.$className + " does not support onRelativeValueChange set in its widget-params 'changeListeners'. Listeners will be ignored.");
}
else
{
for (let event of Object.keys(changeListeners))
{
let paths = Ext.Array.from(changeListeners[event]);
for (let path of paths)
{
let data = {
grid: this.grid
};
if (path.startsWith('..'))
{
data.dataPath = arguments[6] + '/' + field.name;
data.record= arguments[7];
}
else
{
data.dataPath = field.name;
data.record= this.getActiveRecord();
}
$class.onRelativeValueChange(event, path, field, Ametys.form.Widget.getRelativeValue(path, data), null);
}
}
}
}
if (field.updateAdditionalWidgetsConf)
{
field.updateAdditionalWidgetsConf({contentInfo: { contentId: this._getContentId(position.record) }});
}
}
return activated;
},
/**
* @private
* Create the save bar when results are edited
* @param {Object} config The constructor configuration
*/
_createSaveBar: function(config)
{
return {
xtype: 'container',
id: this.getId() + "-savebar",
cls: 'hint',
dock: 'top',
hidden: true,
border: false,
layout: {
type: 'hbox',
pack: 'start',
align: 'stretch'
},
items: [
{
xtype: 'component',
cls: 'a-text',
html: "{{i18n UITOOL_CONTENTEDITIONGRID_SAVEBAR_MESSAGE}}",
flex: 1
},
{
xtype: 'button',
width: 100,
height: 22,
text: "{{i18n UITOOL_CONTENTEDITIONGRID_SAVEBAR_SAVE_LABEL}}",
handler: function() { this._saveEdition(); },
scope: this
},
{
xtype: 'button',
width: 100,
height: 22,
text: "{{i18n UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_LABEL}}",
handler: Ext.bind(this.discardChanges, this, [true, null]),
scope: this
}
]
};
},
/**
* Get the save bar
* @return {Ext.toolbar.Paging} The save toolbar
*/
getSaveBar: function()
{
return Ext.getCmp(this.getId() + "-savebar");
},
/**
* @protected
* Get the content id of the given record
* @param {Ext.data.Model} record The record involved
* @return {String} The content id of the record
*/
_getContentId: function(record)
{
return record.getId();
},
/**
* @protected
* Get the record by content id
* @param {String} contentId The content id to consider
* @return {Ext.data.Model[]} The records representing this content
*/
_findRecords: function(contentId)
{
return [this.getStore().getById(contentId)];
},
/**
* Locks a content when entering in edition
* @param {Ext.grid.plugin.CellEditing} editor The editor plugin
* @param {Object} e An edit event with the following properties:
* @param {Ext.grid.Panel} e.grid The grid
* @param {Ext.data.Model} e.record The record being edited
* @param {String} e.field The field name being edited
* @param {Object} e.value The value for the field being edited.
* @param {HTMLElement} e.row The grid table row
* @param {Ext.grid.column.Column} e.column The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
* @param {Number} e.rowIdx The row index that is being edited
* @param {Number} e.colIdx The column index that is being edited
* @param {boolean} e.cancel Set this to true to cancel the edit or return false from your handler.
* @private
*/
_beforeEdit: function(editor, e)
{
let field = e.column.getEditor();
if (field.isVisible(true) && editor.activeEditor == e.record)
{
// sometimes before edit is thrown twice in a row
return false;
}
if (e.value === undefined && field.defaultValue !== undefined)
{
field.switchToValue = field.defaultValue;
}
else
{
field.switchToValue = undefined;
}
if (window.event)
{
var originalEvent = Ext.create(Ext.event.Event, window.event);
if (originalEvent.target && /^a$/i.test(originalEvent.target.tagName))
{
// We do not want to edit here. The user was clicking on the link!
return false;
}
}
// Is this field modifiable for the current user?
if ((e.record.data['notEditableData'] === true
|| e.record.data['notEditableDataIndex'] && Ext.Array.contains(e.record.data['notEditableDataIndex'], e.field))
|| Ext.fly(e.cell).hasCls('cell-disabled'))
{
// Repeater widget need to be always clickable (when not empty) to be able to see values
if (e.column.type != 'repeater'
|| e.value._size == 0
|| e.record.data['notClickableRepeater'] && Ext.Array.contains(e.record.data['notClickableRepeater'], e.field))
{
this.getLogger().debug("Cannot edit field " + e.field + " for content " + this._getContentId(e.record));
return false;
}
}
// Is an external attribute
else if (e.record.data[e.field + '_external_status'] == 'external')
{
this.getLogger().debug("Cannot edit field " + e.field + " for content " + this._getContentId(e.record) + " because synchronization status is currently external");
return false;
}
// Has the computing already been done ?
else if (e.record.data['notEditableData'] !== true && !e.record.data['notEditableDataIndex'])
{
Ametys.cms.content.ContentDAO.getContent(this._getContentId(e.record), Ext.bind(this._beforeEditContentCB, this, [e.record], 1));
var editableMetadata = [];
var columns = this.getColumns();
for (let column of columns)
{
if (column.getEditor() != null)
{
editableMetadata.push(column.dataIndex);
}
}
e.record.set('notEditableDataIndex', []);
Ametys.data.ServerComm.callMethod({
role: 'org.ametys.cms.content.ContentHelper',
methodName: 'getContentAttributeDefinitionsAsJSON',
parameters: [this._getContentId(e.record), editableMetadata, true],
priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
callback: {
handler: Ext.bind(this._beforeEditContentCB2, this, [e.record], true),
scope: this
},
waitMessage: true,
errorMessage: {
msg: "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORMDEFINITION_ERROR}} '" + this._getContentId(e.record) + "'",
category: Ext.getClassName(this)
}
});
}
else if (this.getModifiedFields(e.record).length == 0)
{
Ametys.cms.content.ContentDAO.getContent(this._getContentId(e.record), Ext.bind(this._beforeEditContentCB, this, [e.record], 1));
}
// After the setValue, the size of the field may have changed... lets realign it
var me = this;
window.setTimeout(function() {
if (me.editingPlugin && me.editingPlugin.activeEditor)
{
me.editingPlugin.activeEditor.realign(true)
}
}, 1)
},
/**
* Callback for #_beforeEdit to get the content associated to the record
* @param {Ametys.cms.content.Content} content The content being edited
* @param {Ext.data.Model} record The record being edited
* @private
*/
_beforeEditContentCB: function(content, record)
{
if (content == null)
{
// An error was already shown to the user by the DAO
record.set('notEditableData', true);
this._doCancelEdition();
}
// Check if edit action is possible
else if (content.getAvailableActions().indexOf(this.workflowEditActionId) == -1)
{
record.set('notEditableData', true);
this._doCancelEdition();
Ametys.log.ErrorDialog.display({
title: "{{i18n UITOOL_CONTENTEDITIONGRID_LOCK_ERROR_TITLE}}",
text: "{{i18n UITOOL_CONTENTEDITIONGRID_LOCK_ERROR_DESC}}",
details: "The content '" + content.getId() + "' cannot be edited with workflow action '" + this.workflowEditActionId + "'. Available actions are " + content.getAvailableActions(),
category: this.self.getName()
});
}
else
{
content.setMessageTarget(this.messageTarget);
// Lock content
this._lockOrUnlockContent(content, true, Ext.bind(this._beforeEditLockCB, this), false);
}
},
/**
* @private
* Callback to determine which fields can be edited
* @param {XMLElement} response The server response
* @param {Object} args Additionnal arguments. Empty
* @param {Ext.data.Model} record The edited record
*/
_beforeEditContentCB2: function(response, args, record)
{
var notEditableDataIndex = [];
function seekUnwritable(obj, prefix)
{
Ext.Object.each(obj, function(key, value, obj) {
if (value['can-not-write'] == true)
{
notEditableDataIndex.push(prefix + key);
}
if (value['type'] == "composite" || value['type'] == "repeater" )
{
seekUnwritable(value.elements, prefix + key + "/");
}
});
}
seekUnwritable(response.attributes.elements, "");
record.set('notEditableDataIndex', notEditableDataIndex);
// reverse uneditable fields
var changes = record.getChanges()
Ext.Object.each(changes, function(key, value) {
if (Ext.Array.contains(record.data['notEditableDataIndex'], key))
{
// revert
record.set(key, record.modified[key]);
}
});
// if revert was successful, save bar may need to be hidden
this._showHideSaveBar();
if (this.editingPlugin.editing && Ext.Array.contains(notEditableDataIndex, this.editingPlugin.getActiveColumn().dataIndex))
{
if (this.editingPlugin.getActiveColumn().type != 'repeater')
{
this._doCancelEdition();
}
else
{
this.editingPlugin.getActiveColumn().field.setReadOnly();
}
}
else
{
// var me = this;
// me.editingPlugin.activeEditor.realign(true)
}
},
/**
* Callback for #_beforeEditContentCB after lock process was done
* @param {boolean} success Is lock ok?
* @private
*/
_beforeEditLockCB: function(success)
{
if (!success)
{
// cancel edition
this._doCancelEdition();
}
// finally, we can, edit normally
},
/**
* @private
* The current edition has to be silently canceled.
*/
_doCancelEdition: function()
{
// this.editingPlugin.suspendEvent('canceledit');
this.editingPlugin.cancelEdit();
// this.editingPlugin.resumeEvent('canceledit');
},
/**
* Unlocks a content when canceling edition (if no other modifications where running)
* @param {Ext.grid.plugin.CellEditing} editor The editor plugin
* @param {Object} e An edit event with the following properties:
* @param {Ext.grid.Panel} e.grid - The grid
* @param {Ext.data.Model} e.record - The record being edited
* @param {String} e.field - The field name being edited
* @param {Object} e.value - The value for the field being edited.
* @param {HTMLElement} e.row - The grid table row
* @param { Ext.grid.column.Column} e.column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
* @param {Number} e.rowIdx - The row index that is being edited
* @param {Number} e.colIdx - The column index that is being edited
* @private
*/
_cancelEdit: function(editor, e)
{
if (this.getModifiedFields(e.record).length == 0)
{
var tmpContent = Ext.create("Ametys.cms.content.Content", { locked: true, id: this._getContentId(e.record), messageTarget: this.messageTarget });
// Unlock content
this._lockOrUnlockContent(tmpContent, false, function (success) { /* ignore */ }, true);
}
},
/**
* Cancel the change if the editing field has trigger the opening of a dialog box
* @param {Ext.grid.plugin.CellEditing} editor The editor plugin
* @param {Object} context The editing context
* @private
*/
_validateEdit: function (editor, context)
{
if (editor.activeEditor && editor.activeEditor.field)
{
return !editor.activeEditor.field.triggerDialogBoxOpened;
}
return true;
},
/**
* When an edition ends, add in the search screen (if necessary) a save/discard toolbar
* @param {Ext.grid.plugin.CellEditing} editor The editor plugin
* @param {Object} e An edit event with the following properties:
* @param {Ext.grid.Panel} e.grid - The grid
* @param {Ext.data.Model} e.record - The record being edited
* @param {String} e.field - The field name being edited
* @param {Object} e.value - The value for the field being edited.
* @param {Object} e.originalValue - The value for the field before the edit
* @param {HTMLElement} e.row - The grid table row
* @param { Ext.grid.column.Column} e.column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
* @param {Number} e.rowIdx - The row index that is being edited
* @param {Number} e.colIdx - The column index that is being edited
* @private
*/
_edit: function(editor, e)
{
// We want to consider approximative equals
if (e.record.modified != undefined && (Ext.isEmpty(e.record.modified[e.field]) && Ext.isEmpty(e.record.data[e.field]))
// We want to compare arrays of values
|| e.record.modified != undefined && Ext.isArray(e.record.modified[e.field]) && Ext.isArray(e.record.data[e.field]) && Ext.Array.equals(e.record.modified[e.field], e.record.data[e.field]))
{
// We will reset such values
e.record.set(e.field, e.record.modified[e.field]);
}
if (e.record.modified == undefined || e.record.modified[e.field] === undefined)
{
this._cancelEdit(editor, e);
}
this._showHideSaveBar();
// Reset after ending, so the next edition may not compare with old values (such as combobox RUNTIME-3972)
e.column.getEditor().reset();
},
/**
* @private
* Remove the search and display the savebar instead (or the opposite) depending on #isModeEdition
*/
_showHideSaveBar: function(forceVisible)
{
let visible = forceVisible !== undefined ? forceVisible : this.isModeEdition();
if (this.withSaveBar)
{
this.getSaveBar().setVisible(visible);
}
if (this.paginationEnabled)
{
this.getPageBar().setDisabled(visible);
}
// prevent sort that would trash modifications
var columns = this.getColumns();
for (var i = 0; i < columns.length; i++)
{
if (visible)
{
columns[i].oldSortable = columns[i].oldSortable || columns[i].sortable;
columns[i].sortable = false;
}
else
{
columns[i].sortable = columns[i].oldSortable || columns[i].sortable;
columns[i].oldSortable = null;
}
}
this.fireEvent("dirtychange", visible);
},
/**
* Is in edition mode?
* @return {boolean} true if any record already is modified
*/
isModeEdition: function()
{
return this.getModifiedRecords().length > 0;
},
/**
* The list of modified records
* @private
*/
getModifiedRecords: function()
{
let modifiedRecords = [];
let records = this.getStore().getModifiedRecords();
if (this.getStore().root && this.getStore().root.modified)
{
records.push(this.getStore().root);
}
for (let record of records)
{
if (this.getModifiedFields(record).length > 0)
{
modifiedRecords.push(record);
}
}
return modifiedRecords;
},
/**
* Get the modified worthable fields in the record
* @param {Ext.data.Model} record The record to check
* @return {String[]} The modified fieldnames
*/
getModifiedFields: function(record)
{
const ignoreFields = ['notEditableData', 'notEditableDataIndex', 'notClickableRepeater', 'leaf'];
if (record.get('notEditableData'))
{
return [];
}
let notEditableDataIndex = record.get('notEditableDataIndex');
if (notEditableDataIndex)
{
for (let nedi of notEditableDataIndex)
{
ignoreFields.push(nedi);
}
}
let modified = Object.keys(record.modified || {});
return modified.filter(v => !ignoreFields.includes(v));
},
/**
* @private
* Save the current edition in the store
* @param {Function} [callback] Called at the end of the save process
* @param {Boolean} callback.success True if the save was done correctly
*/
_saveEdition: function(callback)
{
let me = this;
callback = callback || Ext.emptyFn;
function getTitle(record, contentId)
{
return Ametys.plugins.cms.search.SearchGridHelper.renderMultilingualString(record.get('title'))
|| record.get('name')
|| contentId;
}
try
{
if (this.editingPlugin.editing)
{
this.editingPlugin.completeEdit();
}
var contentIds = [];
var invalidContentTitles = [];
Ext.Array.forEach(this.getModifiedRecords(), function(record) {
if (me._isValid(me, record))
{
let contentId = me._getContentId(record);
if (!contentIds.includes(contentId))
{
contentIds.push(contentId);
}
}
else
{
let contentId = me._getContentId(record);
if (!invalidContentTitles.includes(contentId))
{
invalidContentTitles.push(getTitle(record, contentId));
}
}
});
if (contentIds.length > 0)
{
if (invalidContentTitles.length > 0)
{
let oldCb = callback;
callback = function()
{
invalidContents(invalidContentTitles)
oldCb.apply(this, arguments);
}
}
Ametys.cms.content.ContentDAO.getContents(contentIds, Ext.bind(this._saveEditionWithContents, this, [callback], 1));
}
else
{
if (invalidContentTitles.length > 0)
{
invalidContents(invalidContentTitles)
}
callback(false);
}
}
catch (e)
{
callback(false);
throw e;
}
function invalidContents(contentsTitle)
{
Ametys.form.SaveHelper.SaveErrorDialog.showErrorDialog("{{i18n UITOOL_CONTENTEDITIONGRID_VALUE_ERROR_TITLE}}", "{{i18n UITOOL_CONTENTEDITIONGRID_VALUE_ERROR_DESC}}<ul><li>" + contentsTitle.join("</li><li>") + "</li></ul>");
}
},
/**
* @private
* Test if the recrod is valid
* @param {Ext.grid.Panel} grid The host grid
* @param {Ext.data.Model} record The record to check
* @return {Boolean} true if all the columns are valid, false otherwise
*/
_isValid: function(grid, record)
{
for (let column of grid.getColumns())
{
if (!Ametys.plugins.cms.search.SearchGridHelper.isValidValue(column, record, undefined, grid.getStore()))
{
return false;
}
}
return true;
},
/**
* @private
* continue the save edition process
* @param {Ametys.cms.content.Content[]} contents The contents modified
* @param {Function} [callback] Called at the end of the save process
* @param {Boolean} callback.success True if the save was done correctly
*/
_saveEditionWithContents: function(contents, callback)
{
try
{
var me = this;
// We are going to loop on all contents
// But we want to call the callback only at the very end of theses asynchronous calls
// So lets ignore the first n-1 calls.
var barrierCallback = Ext.Function.createBarrier(contents.length, callback);
Ext.Array.forEach(contents, function(content) {
me._doSaveEdition(content, false, barrierCallback);
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.WORKFLOW_CHANGING,
targets: {
id: me.messageTarget,
parameters: { contents: [content] }
}
});
});
}
catch (e)
{
callback(false);
throw e;
}
if (contents && contents.length == 0)
{
// no content modified
callback(true);
}
},
/**
* @private
* Retrieves the given record's changes prefixed with the given prefix
* @param {Ext.data.Model} record The record
* @param {String} prefix the prefix
* @return {Object} the prefixed record's changes
*/
_getPrefixedChanges: function(record, prefix)
{
var changesBefore = record.getChanges();
var changesAfter = {};
Ext.Array.each(record.getFields(), function (field) {
var fieldName = field.getName(),
value = changesBefore[fieldName];
if (value === undefined && Object.keys(changesBefore).includes(fieldName))
{
value = null;
}
var key = fieldName.indexOf("_") === 0 ? "_" + prefix + fieldName.substring(1) : prefix + fieldName;
if (field.ftype == "repeater" && Ext.isObject(value))
{
// Repeaters
Ext.Object.each(value, function (keySuffix, subValue) {
if (!Ext.String.endsWith(keySuffix, "__externalDisableConditionsValues"))
{
if (keySuffix.indexOf("_") === 0 && keySuffix.indexOf("[") === 1)
{
changesAfter["_" + key + keySuffix.substring(1)] = subValue;
}
else if (keySuffix.indexOf("_") === 0)
{
changesAfter["_" + key + "/" + keySuffix.substring(1)] = subValue;
}
else if (keySuffix.indexOf("[") === 0)
{
changesAfter[key + keySuffix] = subValue;
}
else
{
changesAfter[key + "/" + keySuffix] = subValue;
}
}
});
}
else if (field.ftype == "date" && Ext.isDate(value))
{
changesAfter[key] = Ext.Date.format(value, Ext.Date.patterns.ISO8601Date);
}
else if (field.ftype == "datetime" && Ext.isDate(value))
{
changesAfter[key] = Ext.Date.format(value, Ext.Date.patterns.ISO8601DateTime);
}
else if (value !== undefined)
{
changesAfter[key] = value;
}
});
return changesAfter;
},
/**
* @private
* Find a field label using its name
* @param {String} fdName The field full name
* @param {String} defaultLabel If not colupmn matches, take this as default value. Otherwise will be the fdName
* @return {String} A readable label
*/
_findColumnLabelByDataIndex: function(fdName, defaultLabel)
{
const me = this;
function seek(columns, dataPaths)
{
for (let column of columns)
{
if ((column.dataIndex || column.name) == dataPaths[0])
{
if (dataPaths.length == 1)
{
return column.text || column.label;
}
else
{
let subcolumns = column.columns;
if (!subcolumns)
{
subcolumns = me.getStore().getModel().getField(dataPaths[0]).subcolumns;
}
return column.text + " (" + dataPaths[1] + ") > " + seek(subcolumns, dataPaths.slice(2));
}
}
}
return null;
}
return seek(this.getColumns(), fdName.split('.'))
|| defaultLabel
|| fdName;
},
/**
* @private
* continue the save edition process
* @param {Object} response The XMLHTTPRequest response object
* @param {Object} args The arguments of the sendMessage
*/
_saveEditionCB: function(response, args)
{
var content = args['content'];
var callback = args['callback'];
if (Ametys.data.ServerComm.handleBadResponse("{{i18n PLUGINS_CMS_SAVE_ACTION_ERROR}}", response, this.self.getName()))
{
// Workflow did not really changed, but WORKFLOW_CHANGING has been sent, elements could be waiting for update
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.WORKFLOW_CHANGED,
targets: {
id: this.messageTarget,
parameters: { contents: [content] }
}
});
callback(false);
return;
}
var detailedMsg = "";
var success = Ext.dom.Query.selectValue ('> ActionResult > success', response) == "true";
// Handling workflow errors
var workflowErrors = Ext.dom.Query.select ('> ActionResult > workflowValidation > error', response);
if (!success && workflowErrors.length > 0)
{
detailedMsg += '<ul>';
for (var i=0; i < workflowErrors.length; i++)
{
var errorMsg = Ext.dom.Query.selectValue("", workflowErrors[i]);
detailedMsg += '<li>' + errorMsg + '</li>';
}
detailedMsg += '</ul>';
Ametys.form.SaveHelper.SaveErrorDialog.showErrorDialog ("\"" + content.getTitle() + "\" {{i18n plugin.core-ui:PLUGINS_CORE_UI_SAVE_ACTION_FAILED_TITLE}}", "{{i18n PLUGINS_CMS_SAVE_ACTION_FAILED_WORKFLOW_DESC}}", detailedMsg);
// Workflow did not really changed, but WORKFLOW_CHANGING has been sent, elements could be waiting for update
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.WORKFLOW_CHANGED,
targets: {
id: this.messageTarget,
parameters: { contents: [content] }
}
});
callback(false);
return;
}
// Handling field errors
var errors = Ext.dom.Query.select ('> ActionResult > * > fieldResult > error', response);
if (!success && errors.length > 0)
{
var msg = "{{i18n PLUGINS_CMS_SAVE_ACTION_FAILED_DESC}}";
msg += errors.length == 1 ? "{{i18n PLUGINS_CMS_SAVE_ACTION_FAILED_DESC_SINGLE}}" : errors.length + "{{i18n PLUGINS_CMS_SAVE_ACTION_FAILED_DESC_MULTI}}";
var detailedMsg = this._getFieldsDetailedMessage(errors);
let dialogTitle = "\"" + content.getTitle() + "\" {{i18n plugin.core-ui:PLUGINS_CORE_UI_SAVE_ACTION_FAILED_TITLE}}";
Ametys.form.SaveHelper.SaveErrorDialog.showErrorDialog (dialogTitle, msg, detailedMsg);
// Workflow did not really changed, but WORKFLOW_CHANGING has been sent, elements could be waiting for update
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.WORKFLOW_CHANGED,
targets: {
id: this.messageTarget,
parameters: { contents: [content] }
}
});
callback(false);
return;
}
// Handling field warnings
var warnings = Ext.dom.Query.select ('> ActionResult > * > fieldResult > warning', response);
if (!success && warnings.length > 0)
{
var msg = warnings.length == 1 ? "{{i18n PLUGINS_CMS_SAVE_ACTION_WARNING_DESC_SINGLE}}" : "{{i18n PLUGINS_CMS_SAVE_ACTION_WARNING_DESC_MULTI}}";
var detailedMsg = this._getFieldsDetailedMessage(warnings);
var question = "{{i18n PLUGINS_CMS_SAVE_ACTION_WARNING_DESC_QUESTION}}";
let dialogTitle = "\"" + content.getTitle() + "\" {{i18n plugin.core-ui:PLUGINS_CORE_UI_SAVE_ACTION_WARNING_TITLE}}";
Ametys.form.SaveHelper.SaveErrorDialog.showWarningDialog (dialogTitle, msg, detailedMsg, question, Ext.bind(this._showWarningsCb, this, [args], 1));
return;
}
var infos = Ext.dom.Query.select ('> ActionResult > * > fieldResult > info', response);
if (success && infos.length > 0)
{
for (var i=0; i < infos.length; i++)
{
var infoData = infos[i].parentNode.parentNode;
var fieldId = infoData.tagName;
let notifTitle = "\"" + Ext.String.escapeHtml(content.getTitle()) + "\" {{i18n plugin.cms:PLUGINS_CMS_SAVE_ACTION_CONTENT_MODIFICATION}}";
var message = Ext.dom.Query.selectValue("", infos[i]);
if (fieldId !== '_global')
{
let defaultLabel = Ext.dom.Query.selectValue ('> fieldLabel', infoData) || fieldId;
var label = this._findColumnLabelByDataIndex(fieldId, defaultLabel);
message = '<b>' + label + '</b><br/>' + message;
}
Ametys.notify({
title: notifTitle,
description: message
});
}
}
// Object content may be outdated, just trust its id
var contentId = content.getId();
// graphically finish the save process
for (let r of this._findRecords(contentId))
{
r.commit(null, Object.keys(r.modified));
}
this._showHideSaveBar();
this._sendMessages(contentId);
callback(true);
},
_sendMessages: function(contentId)
{
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.MODIFIED,
targets: {
id: this.messageTarget,
parameters: { ids: [contentId] }
}
});
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.LOCK_CHANGED,
targets: {
id: this.messageTarget,
parameters: { ids: [contentId] }
}
});
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.WORKFLOW_CHANGED,
targets: {
id: this.messageTarget,
parameters: { ids: [contentId] }
}
});
},
/**
* @private
* Callback function called after user chose to ignore warnings or not
* @param {Boolean} ignoreWarnings true if the user choose to ignore warnings and save anyway, false otherwise
* @param {Object} args The arguments of the sendMessage
*/
_showWarningsCb: function (ignoreWarnings, args)
{
var content = args['content'];
var callback = args['callback'];
if (ignoreWarnings)
{
this._doSaveEdition(content, true, callback);
}
else
{
// Workflow did not really changed, but WORKFLOW_CHANGING has been sent, elements could be waiting for update
Ext.create("Ametys.message.Message", {
type: Ametys.message.Message.WORKFLOW_CHANGED,
targets: {
id: this.messageTarget,
parameters: { contents: [content] }
}
});
callback(false);
}
},
/**
*
*/
_doSaveEdition: function(content, ignoreWarnings, callback)
{
var me = this;
var record = me._findRecords(content.getId())[0]; // we can take the first record since all records should display the same value
var params = {};
params.values = me._getPrefixedChanges(record, "content.input.");
params.contentId = me._getContentId(record);
params.quit = true;
params['content.view'] = null;
params['local.only'] = true;
params['ignore.warnings'] = ignoreWarnings;
Ametys.data.ServerComm.send({
plugin: 'cms',
url: 'do-action/' + me.workflowEditActionId,
parameters: params,
waitMessage: {
target: me,
msg: "{{i18n CONTENT_EDITION_SAVING}}"
},
priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
callback: {
scope: me,
handler: me._saveEditionCB,
arguments: {
content: content,
callback: callback
}
}
});
},
/**
* @private
* Retrieves detailed message for given fields
* @param {HTMLElement} fields The XML containing fields and correspondin messages
*/
_getFieldsDetailedMessage: function (fields)
{
var detailedMessage = '<ul>';
for (var i=0; i < fields.length; i++)
{
var fieldData = fields[i].parentNode.parentNode;
var fieldId = fieldData.tagName;
var message = Ext.dom.Query.selectValue("", fields[i]);
if (fieldId == '_global')
{
detailedMessage += '<li>' + message + '</li>';
}
else
{
let defaultLabel = Ext.dom.Query.selectValue ('> fieldLabel', fieldData) || fieldId;
var label = this._findColumnLabelByDataIndex(fieldId, defaultLabel);
detailedMessage += '<li><b>' + label + '</b><br/>' + message + '</li>';
}
}
detailedMessage += '</ul>';
return detailedMessage;
},
/**
* Discard modifications
* @param {Boolean} prompt Ask user
* @param {Function} callback The callback if discard is accepted, when done
*/
discardChanges: function(prompt, callback)
{
if (prompt != false)
{
// Quit the edition mode ?
var me = this;
Ametys.Msg.confirm("{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_LABEL}}",
"{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_DESC}}",
Ext.bind(this._unsaveEditionCB, this, [callback], 1)
);
}
else
{
this._unsaveEditionCB('yes', callback);
}
},
/**
* @private
* Discard the current edition in the store
* @param {String} answer 'yes' to effectively do it
* @param {Function} [callback] The function to call after the process is over
*/
_unsaveEditionCB: function(answer, callback)
{
let me = this;
if (answer != 'yes')
{
return;
}
if (this.editingPlugin.editing)
{
this._doCancelEdition();
}
Ametys.data.ServerComm.suspend();
let modifiedRecords = this.getModifiedRecords();
Ext.Array.forEach(modifiedRecords, function(record) {
var tmpContent = Ext.create("Ametys.cms.content.Content", { locked: true, id: me._getContentId(record), messageTarget: this.messageTarget });
this._lockOrUnlockContent(tmpContent, false, function (success) { /* ignore */ }, true);
// Reseting manually advanced data (repeater, content...)
Ext.Object.each(record.data, function(name, value) {
if (Ext.String.endsWith(name, "_initial"))
{
let initialName = name.substring(0, name.length - "_initial".length);
record.data[initialName] = Ext.clone(value);
}
})
}, this);
Ametys.data.ServerComm.restart();
this.getStore().rejectChanges();
if (this.getStore().root)
{
this.getStore().root.reject()
}
this._showHideSaveBar();
// TODO
// Reload relative fields
for (let record of modifiedRecords)
{
this.getStore().checkChangeNow(this.getStore(), record, null, Object.keys(this.changeListeners), null, null);
}
if (callback)
{
callback();
}
},
/**
* Lock or unlock a content and call it's callback.
* If lock is asked:
* - the content will not be locked if unlock have been called without a previous lock.
* - if multiple locks are done, no further network calls will be made (instead an unlock occurs)
* If unlock is asked, if multiple unlocks are done, no further network calls will be made (instead a lock occurs)
* @param {Ametys.cms.content.Content} content content to lock or unlock
* @param {Boolean} lock true to lock the content, false to unlock it
* @param {Function} callback The method to call
* @param {Boolean} callback.success true if the lock was a success, else false
* @param {Boolean} force force lock without checking the content inner status
* @param {Boolean} [internal] true for internal call (do not add lock/unlock to the stack)
* @private
*/
_lockOrUnlockContent: function (content, lock, callback, force, internal)
{
if (this.destroyed != false)
{
return;
}
var contentId = content.getId();
if (!this._lockedMap[contentId])
{
// Add content to the map of object to be lock/unlock
this._lockedMap[contentId] = {
state : Ametys.cms.content.EditContentsView._LOCK_STATE_IDLE,
count : 0,
actions : []
}
}
// Add lock or unlock action to the stack (except for internal call)
if (!internal)
{
this._lockedMap[contentId].actions.push({
action : lock ? "lock" : 'unlock',
callback : callback
});
}
var count = this._lockedMap[contentId].count;
var state = this._lockedMap[contentId].state;
if (state == Ametys.cms.content.EditContentsView._LOCK_STATE_IDLE)
{
if (lock && count < 0)
{
var me = this,
remainingActions = [];
// Invoke callback functions for all lock actions and keep unlock actions only
Ext.Array.each(this._lockedMap[contentId].actions, function (action) {
if (action.action == "lock")
{
if (Ext.isFunction(action.callback))
{
action.callback(true);
}
me._lockedMap[contentId].count++;
}
else
{
remainingActions.push(action);
}
});
// Update remaining actions
this._lockedMap[contentId].actions = remainingActions;
if (this._lockedMap[contentId] && this._lockedMap[contentId].count == 0 && this._lockedMap[contentId].actions.length == 0)
{
delete this._lockedMap[contentId];
}
}
else
{
// Do lock or unlock action
this._lockedMap[contentId].state = Ametys.cms.content.EditContentsView._LOCK_STATE_WORKING;
if (lock)
{
content.lock(Ext.bind(this._lockOrUnlockContentCb, this, [contentId, lock], true), force);
}
else
{
content.unlock(Ext.bind(this._lockOrUnlockContentCb, this, [contentId, lock], true), force);
}
}
}
},
/**
* @private
* Callback function invoked after locking or unlocking a content
* @param {Boolean} success true if successfully changed lock state
* @param {String} contentId the id of content being locked or unlock
* @param {Boolean} lock true if the content was locked, false it was unlocked
*/
_lockOrUnlockContentCb: function (success, contentId, lock)
{
if (this.destroyed != false)
{
return;
}
if (this._lockedMap[contentId])
{
this._lockedMap[contentId].state = Ametys.cms.content.EditContentsView._LOCK_STATE_IDLE;
var me = this,
remainingActions = [],
tmpCount = this._lockedMap[contentId].count;
// Invoke callback functions for same actions of the stack and keep invert actions
Ext.Array.each(this._lockedMap[contentId].actions, function (action) {
if ((lock && action.action == "lock") || (!lock && action.action == 'unlock'))
{
if (Ext.isFunction(action.callback))
{
action.callback(success);
}
me._lockedMap[contentId].count = me._lockedMap[contentId].count + (lock ? 1 : -1);
tmpCount = tmpCount + (lock ? 1 : -1);
}
else
{
remainingActions.push(action);
tmpCount = tmpCount + (lock ? -1 : 1);
}
});
// Update remaining actions
this._lockedMap[contentId].actions = remainingActions;
// If there are invert actions to do (some nasty unlock came in play), let's do it
if (!Ext.isEmpty(remainingActions))
{
if ((lock && tmpCount > 0) || (!lock && tmpCount < 0))
{
this._lockedMap[contentId].actions = [];
Ext.Array.each(remainingActions, function (action) {
// just do callbacks
me._lockedMap[contentId].count = me._lockedMap[contentId].count + (lock ? -1 : 1)
if (Ext.isFunction(action))
{
action.callback(success); // FIXME with success ??
}
});
}
else
{
var dummyContent = Ext.create("Ametys.cms.content.Content", { locked: lock, id: contentId, messageTarget: this.messageTarget });
this._lockOrUnlockContent(dummyContent, !lock, null, true, true);
}
}
if (this._lockedMap[contentId] && this._lockedMap[contentId].count == 0 && this._lockedMap[contentId].actions.length == 0)
{
delete this._lockedMap[contentId];
}
}
}
});