/**
* Copyright (C) 2005-2016 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* <p>This extends the [AlfSortablePaginatedList]{@link module:alfresco/lists/AlfSortablePaginatedList}
* to provide support additional filtering of the displayed items. This widget does not perform
* any client side filtering, it simply controls the payloads published to services -
* successful filtering is determined by the ability of the service and the REST API ultimately called to
* support it.</p>
*
* <p>The widgets to control the filters should be defined in the
* [widgetsForFilters]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} and these will
* be rendered above the list. The widgets will typically be
* [form controls]{@link module:alfresco/forms/controls/BaseFormControl} (for example
* a [TextBox]{@link module:alfresco/forms/controls/TextBox} might be used to allow the user to
* enter text that an item property must match to be displayed). As well as defining the widgets it
* is also necessary to configure the
* [filteringTopics]{@link module:alfresco/lists/AlfFilteredList#filteringTopics} that they will
* publish. When one of these topics is published the list will make a new request for updated
* data based on the changed filter value.</p>
*
* <p>It is possible to also display a summary of the currently applied filtered by
* configuring [showFilterSummary]{@link module:alfresco/lists/AlfFilteredList#showFilterSummary}
* to be true. This will provide an at a glance display of all the filters currently in user. Filters
* can also be derived from and set on the browser URL hash by configuring
* [useHash]{@link module:alfresco/lists/AlfHashList#useHash} to be true.</p>
*
* @example <caption>A list of users with an additinal text box for filtering the results.</caption>
* {
* name: "alfresco/lists/AlfFilteredList",
* config: {
* loadDataPublishTopic: "ALF_GET_USERS",
* filteringTopics: ["_valueChangeOf_FILTER"],
* widgetsForFilters: [
* {
* name: "alfresco/forms/controls/TextBox",
* config: {
* fieldId: "FILTER",
* name: "filter",
* placeHolder: "Enter filter text...",
* label: "Name"
* }
* }
* ],
* widgets: [
* {
* name: "alfresco/lists/views/HtmlListView",
* config: {
* propertyToRender: "userName"
* }
* }
* ]
* }
* }
*
* @module alfresco/lists/AlfFilteredList
* @extends module:alfresco/lists/AlfSortablePaginatedList
* @mixes module:alfresco/core/ObjectProcessingMixin
* @author Dave Draper
*/
define(["dojo/_base/declare",
"alfresco/lists/AlfSortablePaginatedList",
"alfresco/core/ObjectProcessingMixin",
"alfresco/core/topics",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/dom-construct",
"dojo/dom-class",
"dijit/registry",
"alfresco/util/hashUtils"],
function(declare, AlfSortablePaginatedList, ObjectProcessingMixin, topics, lang, array, domConstruct, domClass, registry, hashUtils) {
return declare([AlfSortablePaginatedList, ObjectProcessingMixin], {
/**
* An array of the i18n files to use with this widget.
*
* @instance
* @type {object[]}
* @default [{i18nFile: "./i18n/AlfFilteredList.properties"}]
*/
i18nRequirements: [{i18nFile: "./i18n/AlfFilteredList.properties"}],
/**
* An array of the CSS files to use with this widget.
*
* @instance cssRequirements {Array}
* @type {object[]}
* @default [{cssFile:"./css/AlfFilteredList.css"}]
*/
cssRequirements: [{cssFile:"./css/AlfFilteredList.css"}],
/**
* Indicates whether or not all [filter widgets]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} will have their
* configuration updated to set their [valueSubscriptionTopic]{@link module:alfresco/forms/controls/BaseFormControl#valueSubscriptionTopic}
* to be a [topic]{@link module:alfresco/core/topics#FILTER_VALUE_CHANGE} dedicated to filter value changes. This is done
* in order to allow removal of filters from the [filter summary]{@link module:alfresco/lists/utilities/FilterSummary} to
* reset the filter form control values.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.54
*/
addFilterValueSubscription: true,
/**
* If the [widgetsForFilters]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} attribute is not overridden
* then then this is the value that will be assigned to the [name]{@link module:alfresco/forms/controls/BaseFormControl#description}
* attribute of the [TextBox]{@link module:alfresco/forms/controls/TextBox} that is rendered as the default filter control.
*
* @instance
* @type {string}
* @default
*/
filterDescription: "filtered.list.filter.description",
/**
* If the [widgetsForFilters]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} attribute is not overridden
* then then this is the value that will be assigned to the [name]{@link module:alfresco/forms/controls/BaseFormControl#label}
* attribute of the [TextBox]{@link module:alfresco/forms/controls/TextBox} that is rendered as the default filter control.
*
* @instance
* @type {string}
* @default
*/
filterLabel: "filtered.list.filter.label",
/**
* If the [widgetsForFilters]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} attribute is not overridden
* then then this is the value that will be assigned to the [name]{@link module:alfresco/forms/controls/BaseFormControl#name}
* attribute of the [TextBox]{@link module:alfresco/forms/controls/TextBox} that is rendered as the default filter control.
*
* @instance
* @type {string}
* @default
*/
filterName: "name",
/**
* If the [widgetsForFilters]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} attribute is not overridden
* then then this is the value that will be assigned to the [name]{@link module:alfresco/forms/controls/BaseFormControl#placeHolder}
* attribute of the [TextBox]{@link module:alfresco/forms/controls/TextBox} that is rendered as the default filter control.
*
* @instance
* @type {string}
* @default
*/
filterPlaceholder: "filtered.list.filter.placeholder",
/**
* If the [widgetsForFilters]{@link module:alfresco/lists/AlfFilteredList#widgetsForFilters} attribute is not overridden
* then then this is the value that will be assigned to the [name]{@link module:alfresco/forms/controls/BaseFormControl#unitsLabel}
* attribute of the [TextBox]{@link module:alfresco/forms/controls/TextBox} that is rendered as the default filter control.
*
* @instance
* @type {string}
* @default
*/
filterUnitsLabel: "filtered.list.filter.unitsLabel",
/**
* This is the string that is used to map the call to [processWidgets]{@link module:alfresco/core/Core#processWidgets}
* to create the defined filters to the resulting callback in [allWidgetsProcessed]{@link module:alfresco/lists/AlfFilteredList#allWidgetsProcessed}
*
* @instance
* @type {string}
* @default
* @since 1.0.35
*/
filterWidgetsMappingId: "FILTERS",
/**
* If this is configured to be true then a [filter summary widget]{@link module:alfresco/lists/utilities/FilterSummary}
* will be added above the list.
*
* @instance
* @type {boolean}
* @default
* @since 1.0.54
*/
showFilterSummary: false,
/**
* This is the string that is used to map the call to [processWidgets]{@link module:alfresco/core/Core#processWidgets}
* to create the [filter summary widget]{@link module:alfresco/lists/utilities/FilterSummary} to the resulting callback
* in [allWidgetsProcessed]{@link module:alfresco/lists/AlfFilteredList#allWidgetsProcessed}
*
* @instance
* @type {string}
* @default
* @since 1.0.54
*/
summaryWidgetsMappingId: "FILTER_SUMMARY",
/**
* This is an optional map of filter values to labels. The map should have filter name attributes that are
* mapped to a sub-map of values to labels, e.g.
*
* @example
* filterSummaryLabelMapping: {
* name: {
* ted: "Edward",
* bob: "Robert"
* },
* age: {
* 10: "Ten",
* 20: "Twenty"
* }
* }
*
* @instance
* @type {object}
* @default
* @since 1.0.84
*/
filterSummaryLabelMapping: null,
/**
* The filter widgets
*
* @instance
* @type {Object[]}
*/
_filterWidgets: null,
/**
* Called after properties mixed into instance
*
* @instance
*/
postMixInProperties: function alfresco_lists_AlfFilteredList__postMixInProperties() {
this.inherited(arguments);
if (this.useHash) {
this.mapHashVarsToPayload = true;
}
},
/**
* @instance
*/
postCreate: function alfresco_lists_AlfFilteredList__postCreate() {
domClass.add(this.domNode, "alfresco-lists-AlfFilteredList");
this.filtersNode = domConstruct.create("div", {
className: "alfresco-lists-AlfFilteredList__filters"
}, this.domNode, "first");
// We need a promise here to address the scenario where XHR requests are made for filtering widgets
// that have not had there dependencies correctly analysed by Surf. This is the case for the ComboBox
// when used in a non-standard locale as language specific validation.js and common.js files are requested.
// Using a promise ensures that filters are only used when they're actually available. See AKU-559 for details
if (this.widgetsForFilters)
{
var filtersModel = lang.clone(this.widgetsForFilters);
// Update the filter form control configuration to set (or override) the valueSubScriptionTopic in order
// that filter changes can be applied to the form control (this will typically occur when a filter is
// removed via the FilterSummary widget).
if (this.addFilterValueSubscription)
{
array.forEach(filtersModel, function(widget) {
lang.setObject("config.valueSubscriptionTopic", topics.FILTER_VALUE_CHANGE, widget);
}, this);
}
this.processObject(["processInstanceTokens"], filtersModel);
this.processWidgets(filtersModel, this.filtersNode, this.filterWidgetsMappingId);
}
else
{
this._filterWidgets = {};
}
if (this.showFilterSummary)
{
this.summaryNode = domConstruct.create("div", {
className: "alfresco-lists-AlfFilteredList__summary"
}, this.filtersNode, "after");
var summaryModel = lang.clone(this.widgetsForFilterSummary);
this.processObject(["processInstanceTokens"], summaryModel);
this.processWidgets(summaryModel, this.summaryNode, this.summaryWidgetsMappingId);
}
this.inherited(arguments);
this.alfSubscribe(topics.FILTER_REMOVED, lang.hitch(this, this.onFilterRemoved));
},
/**
* This function can be extended in order to perform additional actions when filters are removed.
*
* @instance
* @param {object} payload The filter item that was removed.
* @since 1.0.54
* @extendable
*/
onFilterRemoved: function alfresco_lists_AlfFilteredList__onFilterRemoved(/*jshint unused:false*/ payload) {
// No action by default - this is just an extension point function.
},
/**
* Extends the [inherited function]{@link module:alfresco/lists/AlfList#allWidgetsProcessed}
* to handle differentiate between filter widget and view widget post creation processing.
*
* @instance
* @param {object[]} widgets The widgets that have been created
* @param {string} processWidgetsId An optional ID that might have been provided to map the results of multiple calls to [processWidgets]{@link module:alfresco/core/Core#processWidgets}
* @since 1.0.34
*/
allWidgetsProcessed: function alfresco_lists_AlfFilteredList__allWidgetsProcessed(widgets, processWidgetsId) {
if (processWidgetsId === "NEW_VIEW_INSTANCE")
{
this.handleNewViewInstances(widgets);
}
else if (processWidgetsId === this.filterWidgetsMappingId)
{
this._storeFilterWidgets(widgets);
this._updateFilterFieldsFromHash();
// Setup the filtering topics based on the filter widgets configured...
array.forEach(this.widgetsForFilters, this.setupFilteringTopics, this);
// Create the subscriptions for the filters once created...
this.createFilterSubscriptions();
}
else if (processWidgetsId === this.summaryWidgetsMappingId)
{
// No action required for filter summary creation
}
else
{
// Only perform the inherited function (e.g. to processViews) when not processing filters
this.registerViews(widgets);
this.completeListSetup();
}
},
/**
* Extends the [inherited function]{@link module:alfresco/lists/AlfList#hideChildren} to ensure that the
* filter controls aren't hid.
*
* @instance
* @param {object} targetNode The node to hide the children of
*/
hideChildren: function alfresco_lists_AlfFilteredList__hideChildren(/*jshint unused:false*/targetNode) {
this.inherited(arguments);
if (this.filtersNode)
{
domClass.remove(this.filtersNode, "share-hidden");
}
if (this.summaryNode)
{
domClass.remove(this.summaryNode, "share-hidden");
}
},
/**
* Extends the [inherited function]{@link module:alfresco/lists/AlfHashList#onFiltersUpdated} to publish
* information about the new filters that are applied to the list.
*
* @instance
* @since 1.0.54
*/
onFiltersUpdated: function alfresco_lists_AlfFilteredList__onFiltersUpdated() {
this.inherited(arguments);
this.alfPublish(topics.FILTERS_APPLIED, this.dataFilters);
},
/**
* We need to make sure any filters in the hash are populated into the dataFilters property
*
* @instance
* @override
* @param {object} payload The publication topic
*/
onHashChange: function alfresco_lists_AlfFilteredList__onHashChange( /*jshint unused:false*/ payload) {
// Only do this when we are mirroring the filters in the hash
if (this.mapHashVarsToPayload) {
// Initialise the data-filters to be all of the filters we have specified, without values
this.dataFilters = array.map(Object.keys(this._filterWidgets), function(filterName) {
return {
name: filterName
};
});
// Filter to only include items currently in the hash and update values
var currHash = hashUtils.getHash();
this.dataFilters = array.filter(this.dataFilters, function(dataFilter) {
dataFilter.value = currHash[dataFilter.name];
return !!dataFilter.value;
});
// Update the filter fields
this._updateFilterFieldsFromHash();
}
// Call inherited
this.inherited(arguments);
},
/**
* Build a collection of filter widgets as a property on this instance
*
* @instance
*/
_storeFilterWidgets: function alfresco_lists_AlfFilteredList___storeFilterWidgets(widgets) {
this._filterWidgets = {};
array.forEach(this.widgetsForFilters, function(filterDef) {
var filterName = filterDef.config.name;
this._filterWidgets[filterName] = array.filter(widgets, function(childWidget) {
return childWidget.name === filterName;
})[0];
}, this);
},
/**
* Update the filter form fields using the filter values in the hash, and update the
* [dataFilters property]{@link module:alfresco/lists/AlfList#dataFilters} at the same time.
*
* @instance
*/
_updateFilterFieldsFromHash: function alfresco_lists_AlfFilteredList___updateFilterFieldsFromHash() {
// Get the hash
var currHash = hashUtils.getHash();
// Run through all of the filter widgets
array.forEach(Object.keys(this._filterWidgets), function(widgetName) {
// Get the widget and the filter value, normalising non-values to null
var widget = this._filterWidgets[widgetName],
filterValue = currHash[widgetName];
if (typeof filterValue === "undefined") {
filterValue = null;
}
// Update the dataFilters
if (filterValue === null) {
this.dataFilters = array.filter(this.dataFilters, function(filter) {
return filter.name !== widgetName;
});
} else {
var filterFound = array.some(this.dataFilters, function(filter) {
if (filter.name === widgetName) {
filter.value = filterValue;
return true;
}
});
if (!filterFound) {
this.dataFilters.push({
name: widgetName,
value: filterValue
});
}
}
// Set the widget value
widget && widget.setValue && widget.setValue(filterValue);
}, this);
this.alfPublish(topics.FILTERS_APPLIED, this.dataFilters || []);
},
/**
* Setups up the [filteringTopics]{@link module:alfresco/lists/AlfList#filteringTopics} for the encapsulated
* [list]{@link module:alfresco/lists/AlfSortablePaginatedList}.
*
* @instance
* @param {object} filter The widget to find a topic from (expected to be a form control)
*/
setupFilteringTopics: function alfresco_lists_AlfFilteredList__setupFilteringTopics(filter) {
if (filter && filter.config && filter.config.fieldId)
{
// See AKU-1076 - ensure filteringTopics do not require explicit configuration
this.filteringTopics = this.filteringTopics || [];
this.filteringTopics.push("_valueChangeOf_" + filter.config.fieldId);
if (this.mapHashVarsToPayload)
{
this.hashVarsForUpdate.push(filter.config.name);
}
}
else
{
this.alfLog("warn", "A configured filter control did not have a fieldId attribute configured", filter, this);
}
},
/**
* This is the default widget model for the filters and defines a single text box that can be
* used as a filter. This can be overridden with any number of filters that are required.
*
* @instance
* @type {array}
*/
widgetsForFilters: [
{
id: "{id}_TEXTBOX",
name: "alfresco/forms/controls/TextBox",
config: {
fieldId: "FILTER",
name: "{filterName}",
placeHolder: "{filterPlaceholder}",
label: "{filterLabel}",
description: "{filterDescription}",
unitsLabel: "{filterUnitsLabel}"
}
}
],
/**
* The default model for rendering a [filter summary]{@link module:alfresco/lists/utilities/FilterSummary}. This
* will only be used if [showFilterSummary]{@link module:alfresco/lists/AlfFilteredList#showFilterSummary} is
* configured to be true.
*
* @instance
* @type {object[]}
* @since 1.0.54
*/
widgetsForFilterSummary: [
{
id: "{id}_FILTER_SUMMARY",
name: "alfresco/lists/utilities/FilterSummary",
config: {
labelMap: "{filterSummaryLabelMapping}"
}
}
]
});
});