/*
* Copyright 2018 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 class provides a widget to select one or more course parts.<br>
* This embeds a component with multiple panels to display course parts, buttons, to edit or remove the course part for each panel and general buttons to add or search a course part.<br>
* Advanced search through a dialog box could be enable. See #cfg-allowSearch.<br>
* Allow content creation using #cfg-allowCreation.<br>
*
* This widget is used to edit course parts in a CFP
*/
Ext.define('Ametys.odf.widget.SelectCoursePart.Repeater', {
extend: 'Ametys.form.AbstractField',
canDisplayComparisons: true,
/**
* @cfg {String} [createButtonText=""] The text of the create content button.
*/
createButtonText: '',
/**
* @cfg {String} [createButtonIconCls="ametysicon-add64"] One or more space separated CSS classes to be applied to the icon of create button. Only used if {@link #createButtonIcon} is null.
* value will be used instead if available.
*/
createButtonIconCls: 'ametysicon-add64',
/**
* @cfg {String} [createButtonIcon] The button icon path for the create content button.
*/
createButtonIcon: null,
/**
* @cfg {String} createButtonTooltip The button icon tooltip for the create button.
*/
createButtonTooltip: "{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_CREATE_BTN_TOOLTIP}}",
/**
* @cfg {String} [searchButtonText=""] The text of the search button.
*/
searchButtonText: '',
/**
* @cfg {String} [searchButtonIconCls="ametysicon-magnifier12"] One or more space separated CSS classes to be applied to the icon of search button. Only used if {@link #searchButtonIcon} is null.
*/
searchButtonIconCls: 'ametysicon-magnifier12',
/**
* @cfg {String} [searchButtonIcon] The button icon path for the search button.
*/
searchButtonIcon: null,
/**
* @cfg {String} searchButtonTooltip The button icon tooltip for the search button.
*/
searchButtonTooltip: "{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_SEARCH_BTN_TOOLTIP}}",
/**
* @cfg {String} [editButtonText=""] The text of the edit button.
*/
editButtonText: '',
/**
* @cfg {String} [editButtonIconCls="ametysicon-edit45"] One or more space separated CSS classes to be applied to the icon of search button. Only used if {@link #editButtonIcon} is null.
*/
editButtonIconCls: 'ametysicon-edit45',
/**
* @cfg {String} [editButtonIcon] The button icon path for the edit content button.
*/
editButtonIcon: null,
/**
* @cfg {String} editButtonToolTip The button icon tooltip for the edit button.
*/
editButtonToolTip: "{{i18n plugin.cms:CONTENT_EDITION_LINK_EDIT_LABEL}}",
/**
* @cfg {String} [removeButtonText=""] The text of the remove button.
*/
removeButtonText: '',
/**
* @cfg {String} [removeButtonIconCls="ametysicon-delete30"] One or more space separated CSS classes to be applied to the icon of search button. Only used if {@link #removeButtonIcon} is null.
*/
removeButtonIconCls: 'ametysicon-delete30',
/**
* @cfg {String} [removeButtonIcon] The button icon path for the remove content button.
*/
removeButtonIcon: null,
/**
* @cfg {String} removeButtonToolTip The button icon tooltip for the remove button.
*/
removeButtonToolTip: "{{i18n plugin.cms:CONTENT_EDITION_LINK_REMOVE_LABEL}}",
/**
* @cfg {String} [emptyText] The message displayed when there is no elements selected.
*/
emptyText: "{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_EMPTY}}",
/**
* @cfg {String} [hintText] The message displayed to select or create a new course part
*/
hintText: "{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_HINT}}",
/**
* @cfg {String} [helpMsg1] The message displayed at the top of the first card of search dialog box
*/
helpMsg1: "{{i18n plugin.cms:PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_SEARCH_HINT}}",
/**
* @cfg {String} [helpMsg2] The message displayed at the top of the second card of search dialog box
*/
helpMsg2: "{{i18n plugin.cms:PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_RESULT_HINT_MULTIPLE}}",
/**
* @cfg {String} [boxTitle] The title of the dialog box.
*/
boxTitle: "{{i18n plugin.cms:PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_TITLE}}",
/**
* @cfg {String} [boxIcon=null] The path of the icon of the dialog box.
*/
boxIcon: null,
/**
* @cfg {String} [boxIconCls] The icon cls
*/
boxIconCls: 'ametysicon-document209 decorator-ametysicon-magnifier12',
/**
* @cfg {Boolean} [allowSearch=true] Set to `false` to disable advanced search.
*/
allowSearch: true,
/**
* @cfg {Boolean} [allowCreation=true] Set to `true` allow content creation. Note that if {@link #creatingContent} is set to 'true', creation will be always disallowed.
*/
allowCreation: true,
/**
* @cfg {Boolean} [readOnly=false] True to disallow add, delete and move actions
*/
readOnly: false,
/**
* @cfg {Boolean} [creatingContent=false] Set to `true` if a content is already being creating.
*/
creatingContent: false,
/**
* cfg {Boolean} [checkCreationRights=true] Set to `false` to not check rights on content types for content creation
*/
checkCreationRights: true,
/**
* @cfg {String} [contentType] The content type to allow search on. If not null, the search will be restricted the search to this content type (and its sub-types).
*/
contentType: null,
/**
* @property {Ametys.cms.content.ContentType/Ametys.cms.content.ContentType[]} _contentTypes Array of content type displayed in the drop down list of the create content dialog box.
* Only used if the create content button is displayed.
* @protected
*/
/**
* @cfg {String} workflowName The id of the workflow initialization function. Only used if the create content button is displayed.
*/
/**
* @cfg {String} [viewForCreation=creation] The id of the view used for creation. Only used if the create content button is displayed.
*/
viewForCreation: 'creation',
/**
* @cfg {String} [viewForModification=default-edition] The id of the view used for modification. Only used if the edit content button is displayed.
*/
viewForModification: 'default-edition',
/**
* @cfg {String} [viewToDisplay=form] The id of the view used to display the content.
*/
viewToDisplay: 'form',
/**
* @cfg {Number} [initWorkflowActionId=1] The id of the initialize workflow action to create the content. Only used if the create content button is displayed.
*/
initWorkflowActionId: 1,
/**
* @cfg {Number} [initAndEditWorkflowActionId=1] The id of the initialize workflow action to create and edit the content. Only used if the create content button is displayed.
*/
initAndEditWorkflowActionId: 1,
/**
* @cfg {Number} [editWorkflowActionId=2] The id of the edition workflow action to edit the content. Only used if the create content button is displayed.
*/
editWorkflowActionId: 2,
/**
* @cfg {String} [modelId=search-ui.coursepart] The id of model to use for search
*/
modelId: 'search-ui.coursepart',
/**
* @cfg {Boolean} [multiple=true] True to allow multiple selection.
*/
multiple: true,
/**
* @property {String} _contentLanguage The language key of the content to create.
* Only used if the create content button is displayed.
* @protected
*/
/**
* @property {String} _contentCatalog The catalog key of the content to create.
* Only used if the create or search content button is displayed.
* @protected
*/
/**
* @private
* @property {Ext.panel.Panel} _repeaterPanel The panel displaying the values.
*/
/**
* @private
* @property {Ext.Component} displayField Component to display hint messages.
*/
/**
* @private
* @property {Boolean} _initialized True if the widget creation process is finished
*/
getValue: function ()
{
if (this._initialized)
{
var value = [];
this._repeaterPanel.items.each(function(item) {
value.push(item.getComponent('id').value);
});
return this.multiple ? value : (value && value.length > 0 ? value[0] : null);
}
else
{
return this.callParent(arguments);
}
},
setValue: function (value)
{
this._rawValue = value;
var me = this;
var ids = [];
value = Ext.isEmpty(value) ? [] : Ext.Array.from(value);
if (value.length == 0
|| this._baseValue !== undefined || this._futureValue !== undefined)
{
this._initialized = true;
if (this._repeaterPanel)
{
this._repeaterPanel.removeAll();
}
if (this._baseValue !== undefined || this._futureValue !== undefined)
{
this._repeaterPanel.getDockedComponent('comparison-message').show();
}
}
else
{
this._initialized = false;
this._repeaterPanel.getDockedComponent('comparison-message').hide();
// Values can be either String (of id) or the model
this._initializedCount = value.length;
Ext.Array.forEach(value, function(v) {
if (!Ext.isString(v))
{
v = v.id;
}
ids.push(v);
me._addItem(v);
});
}
me.callParent([ids]);
},
/**
* When used in readonly mode, settting the comparison value will display ins/del tags
* @param {String} otherValue The value to compare the current value with
* @param {boolean} base When true, the value to compare is a base version (old) ; when false it is a future value
*/
setComparisonValue: function(otherValue, base)
{
if (base)
{
this._baseValue = otherValue || null;
this._futureValue = undefined;
}
else
{
this._baseValue = undefined;
this._futureValue = otherValue || null;
}
this.setValue(this._rawValue);
},
getSubmitValue: function ()
{
return this.multiple ? Ext.encode(this.getValue()) : (this.getValue() || '');
},
initComponent: function()
{
this._initialized = false;
this.cls = 'x-form-selectcontent-widget';
// This widget is desigend solely to work in a CFP when editing a course
// We can set the context once and for all instead of using a updateAdditionalWidgetsConf method
this._initiliazeContentInfo();
this.items = this.getItems();
this.callParent(arguments);
},
/**
* Initialize some informations from the current content to create and get contents with similar informations (language and catalog).
*/
_initiliazeContentInfo: function()
{
if (this.contentInfo && this.contentInfo.contentId != null)
{
// Get the catalog of the current content
Ametys.data.ServerComm.callMethod({
role: "org.ametys.odf.catalog.CatalogsManager",
methodName: "getContentCatalog",
parameters: [this.contentInfo.contentId],
callback: {
handler: this._setContentCatalogCb,
scope: this
},
waitMessage: false
});
// If a context is provided with a content id but without a lang
// set the lang of the content as the lang for the context
if (this.contentInfo.lang == null)
{
Ametys.cms.content.ContentDAO.getContent(this.contentInfo.contentId, c => {
this.contentInfo.lang = c.getLang();
})
}
}
},
/**
* Set the current content catalog.
* @param {String} catalogName The catalog name
* @private
*/
_setContentCatalogCb: function(catalogName)
{
this.contentInfo.catalog = catalogName;
},
/**
* Update the panel which displayed the content.
* @param {Ext.Panel} item The panel to update
* @param {String} contentId The ID of the content to update
* @private
*/
_updateItem: function(item, contentId)
{
var params = {
contentId: contentId,
viewName: this.viewToDisplay,
currentParentId: this.contentInfo.contentId
};
Ametys.data.ServerComm.send({
plugin: 'odf',
url: 'coursepart/view.xml',
parameters: params,
priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
callback: {
handler: this._updateItemCb,
scope: this,
arguments: [item]
},
responseType: 'xml'
});
},
/**
* Called after the _updateItem function, update the panel from the server response.
* @param {HTMLElement} response The server response as XML
* @param {Object[]} args The first arg is the panel to update
* @param {Ext.Panel} args.0 The panel to update
*/
_updateItemCb: function(response, args)
{
args[0].setTitle(Ext.dom.Query.selectValue("title", response));
args[0].getComponent('display').update(Ext.dom.Query.selectNode("display", response).outerHTML);
},
/**
* Open a dialog box to modify the content.
* @param {Ext.Panel} panel The panel to update after the modification.
* @param {Ametys.cms.content.Content} content The content to modify
* @private
*/
_editItem: function(panel, content)
{
var openParams = {
content: content,
editWorkflowActionId: this.editWorkflowActionId,
workflowName: this.workflowName,
viewName: this.viewForModification,
helpmessage1: "{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_EDIT_ALERT}}"
}
this.triggerDialogBoxOpened = true;
Ametys.cms.uihelper.EditContent.open(openParams, Ext.bind(this._editItemCb, this, [panel], true), this);
},
/**
* Called after _editItem, it update the content panel.
* @param {String} contentId The ID of the modified content
* @param {Ext.Panel} panel The panel to update after the modification.
*/
_editItemCb: function(contentId, panel)
{
if (contentId)
{
this._updateItem(panel, contentId);
}
this.triggerDialogBoxOpened = false;
this.focus();
},
/**
* Remove a content panel from the form.
* @param {Ext.Panel} panel The panel to remove, on saving content, the linked content will be removed.
* @private
*/
_removeItem: function(panel)
{
// Confirm deletion
Ametys.Msg.confirm(
"{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_REMOVE_TITLE}}",
"{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_REMOVE_CONFIRM}}",
function (answer)
{
if (answer == 'yes')
{
this._repeaterPanel.remove(panel);
if (this._repeaterPanel.items.getCount() == 0)
{
this.displayField.update(this.emptyText);
}
this.fireEvent('change');
}
},
this
);
},
/**
* Create a panel with the displayed content and add it to the "repeater" panel.
* @param {Ametys.cms.content.Content} content The content to display
* @private
*/
_addItemPanel: function(content)
{
if (this._repeaterPanel.getDockedComponent('comparison-message').isVisible())
{
return;
}
var title = content.getTitle();
var item = Ext.create('Ext.Panel', {
title: title,
layout: {
type: 'anchor'
},
anchor: '100%',
ui: 'light',
titleCollapse: true,
hideCollapseTool: false,
collapsible: true,
header: {
titlePosition: 1
},
border: true,
collapsed: true, // Render the items collapsed by default
// The tools
tools: this.readOnly ? null : [{
text: this.editButtonText,
iconCls: this.editButtonIconCls,
icon: this.editButtonIcon,
tooltip: this.editButtonToolTip,
callback: function(panel)
{
this._editItem(panel, content);
},
scope: this
},{
text: this.removeButtonText,
iconCls: this.removeButtonIconCls,
icon: this.removeButtonIcon,
tooltip: this.removeButtonToolTip,
callback: this._removeItem,
scope: this
}],
items: [{
xtype: 'component',
itemId: 'display',
cls: 'a-panel-text-empty',
border: false,
padding: '0 15 0 15',
html: ''
}, {
xtype: 'hidden',
itemId: 'id',
value: content.getId()
}]
});
this._updateItem(item, content.getId());
this._repeaterPanel.add(item);
if (this.displayField)
{
this.displayField.update(this.hintText);
}
if (this._initializedCount > 0)
{
this._initializedCount--;
if (this._initializedCount == 0)
{
this._initialized = true;
}
}
else
{
this._initialized = true; // On first edition, setValue is not called, so _initialized may not be true, but indeed we are
// On change only when not at startup
this.fireEvent('change');
}
},
/**
* Create the panel with the new item.
* @param {String} contentId The content ID of the content to add. If the selected content is already linked to the current content. It won't be added.
* @private
*/
_addItem: function(contentId)
{
var alreadyAdded = this.multiple ? this.getValue() && this.getValue().indexOf(contentId) >= 0 : this.getValue() === contentId;
if (!alreadyAdded)
{
Ametys.cms.content.ContentDAO.getContent(contentId, Ext.bind(this._addItemPanel, this));
}
},
/**
* Get the items composing the fields
* @return {Object[]} The items
*/
getItems: function()
{
var bbarItems = [];
if (!this.readOnly)
{
this.displayField = Ext.create('Ext.Component', {
cls: Ametys.form.AbstractField.READABLE_TEXT_CLS,
html: this.emptyText,
flex: 1
});
bbarItems.push(this.displayField);
if (this.allowSearch == true || this.allowSearch == 'true')
{
bbarItems.push({
xtype: 'button',
text: this.searchButtonText,
iconCls: this.searchButtonIcon ? null : this.searchButtonIconCls,
icon: this.searchButtonIcon,
tooltip: this.searchButtonTooltip,
handler: this.selectContentsBySearch,
scope: this
});
}
// Button that opens the create content dialog box.
if (this.contentType && (this.allowCreation == true || this.allowCreation == 'true') && !this.creatingContent)
{
// Get the content type and its sub-content types allowed to be created by the user
var checkRights = this.checkCreationRights !== false;
var contentTypesCollection = Ametys.cms.content.ContentTypeDAO.getContentType(this.contentType).getDescendantsOrSelf().createFiltered(function (contentType) {
return (contentType.hasRight() || !checkRights)
&& !contentType.isMixin()
&& !contentType.isAbstract();
});
if (contentTypesCollection.count() > 0)
{
bbarItems.push({
xtype: 'button',
text: this.createButtonText,
iconCls: this.createButtonIcon ? null : this.createButtonIconCls,
icon: this.createButtonIcon,
tooltip: this.createButtonTooltip,
handler: this.openCreateContentBox,
scope: this,
hidden: false
});
this._contentTypes = contentTypesCollection.items;
}
}
}
var bbar = Ext.create('Ext.Panel', {
layout: {
type: 'hbox'
},
items: bbarItems
});
this._repeaterPanel = Ext.create('Ext.panel.Panel', {
items: [],
bbar: bbar,
dockedItems: [{
dock: 'top',
itemId: "comparison-message",
xtype: "component",
html: "<i>{{i18n PLUGINS_ODF_WIDGET_SELECT_COURSEPART_COMPARISON}}</i>",
hidden: true
}]
});
var items = [this._repeaterPanel];
return items;
},
/**
* Open the dialog box to search contents.
*/
selectContentsBySearch: function()
{
this.triggerDialogBoxOpened = true;
Ametys.cms.uihelper.SelectContentBySearch.open({
icon: this.boxIcon,
iconCls: this.boxIconCls,
title: this.boxTitle,
helpmessage1: this.helpMsg1,
helpmessage2: this.helpMsg2,
callback: Ext.bind(this._selectContentsCb, this),
multiple: this.multiple,
modelId: this.modelId,
contentType: this.contentType,
searchModelParameters: {"courseId": this.contentInfo.contentId},
forceMode: 'disabled',
forceValues: {
'reference-catalog-eq': this.contentInfo.catalog,
'reference-contentLanguage-eq': this.contentInfo.lang
}
});
},
/**
* This function is called after selecting contents in the search dialog box.
* Sets the value of the field.
* @param {String|String[]} contentIds The identifiers of selected content
* @private
*/
_selectContentsCb: function(contentIds)
{
if (contentIds)
{
Ext.Array.forEach(contentIds, this._addItem, this);
}
this.triggerDialogBoxOpened = false;
this.focus();
},
/**
* Open the dialog box to create a content.
*/
openCreateContentBox: function()
{
var openParams = {
parentContentType: this.contentType,
contentTypes: this._contentTypes,
contentLanguage: this.contentInfo.lang,
initWorkflowActionId: this.initWorkflowActionId,
initAndEditWorkflowActionId: this.initAndEditWorkflowActionId,
editWorkflowActionId: this.editWorkflowActionId,
workflowName: this.workflowName,
viewName: this.viewForCreation,
forceMode: 'disabled',
forceValues: {
catalog: this.contentInfo.catalog,
courseHolder: this.contentInfo.contentId
},
additionalWorkflowParameters: {
'org.ametys.odf.workflow.AbstractCreateODFContentFunction$catalog': this.contentInfo.catalog,
'org.ametys.odf.workflow.AbstractCreateODFContentFunction$courseHolder': this.contentInfo.contentId
},
hideTitle: true,
helpmessage1: "{{i18n plugin.odf:PLUGINS_ODF_WIDGET_SELECT_COURSEPART_CREATE_MSG}}"
}
this.triggerDialogBoxOpened = true;
Ametys.cms.uihelper.CreateContent.open(openParams, this._openCreateContentBoxCb, this);
},
/**
* This function is called after a content has been created with the create content button.
* Add the content to the select field and open the new content in the content tool (in view mode).
* @param {String} contentId The identifier of created content
* @private
*/
_openCreateContentBoxCb: function(contentId)
{
if (contentId)
{
this._addItem(contentId);
}
this.triggerDialogBoxOpened = false;
this.focus();
},
});