4 * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
5 * @author Cornelius Weiss <c.weiss@metaways.de>
6 * @copyright Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
8 Ext.ns('Tine.widgets.grid');
11 * tine 2.0 app grid panel widget
13 * @namespace Tine.widgets.grid
14 * @class Tine.widgets.grid.GridPanel
17 * <p>Application Grid Panel</p>
21 * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
22 * @author Cornelius Weiss <c.weiss@metaways.de>
24 * @param {Object} config
26 * Create a new GridPanel
28 Tine.widgets.grid.GridPanel = function(config) {
29 Ext.apply(this, config);
31 this.gridConfig = this.gridConfig || {};
32 this.defaultSortInfo = this.defaultSortInfo || {};
33 this.defaultPaging = this.defaultPaging || {
38 // autogenerate stateId
39 if (this.stateful !== false && ! this.stateId) {
40 this.stateId = this.recordClass.getMeta('appName') + '-' + this.recordClass.getMeta('recordName') + '-GridPanel';
43 if (this.stateId && Ext.isTouchDevice) {
44 this.stateId = this.stateId + '-Touch';
47 Tine.widgets.grid.GridPanel.superclass.constructor.call(this);
50 Ext.extend(Tine.widgets.grid.GridPanel, Ext.Panel, {
52 * @cfg {Tine.Tinebase.Application} app
53 * instance of the app object (required)
57 * @cfg {Object} gridConfig
58 * Config object for the Ext.grid.GridPanel
62 * @cfg {Ext.data.Record} recordClass
63 * record definition class (required)
67 * @cfg {Ext.data.DataProxy} recordProxy
71 * @cfg {Tine.widgets.grid.FilterToolbar} filterToolbar
75 * @cfg {Boolean} evalGrants
76 * should grants of a grant-aware records be evaluated (defaults to true)
80 * @cfg {Boolean} filterSelectionDelete
81 * is it allowed to deleteByFilter?
83 filterSelectionDelete: false,
85 * @cfg {Object} defaultSortInfo
87 defaultSortInfo: null,
89 * @cfg {Object} storeRemoteSort
91 storeRemoteSort: true,
93 * @cfg {Boolean} usePagingToolbar
95 usePagingToolbar: true,
97 * @cfg {Object} defaultPaging
101 * @cfg {Object} pagingConfig
102 * additional paging config
106 * @cfg {Tine.widgets.grid.DetailsPanel} detailsPanel
107 * if set, it becomes rendered in region south
111 * @cfg {Array} i18nDeleteQuestion
112 * spechialised strings for deleteQuestion
114 i18nDeleteQuestion: null,
116 * @cfg {String} i18nAddRecordAction
117 * spechialised strings for add action button
119 i18nAddActionText: null,
121 * @cfg {String} i18nEditRecordAction
122 * specialised strings for edit action button
124 i18nEditActionText: null,
126 * @cfg {String} i18nMoveActionText
127 * specialised strings for move action button
129 i18nMoveActionText: null,
131 * @cfg {Array} i18nDeleteRecordAction
132 * specialised strings for delete action button
134 i18nDeleteActionText: null,
136 * Tree panel referenced to this gridpanel
141 * if this resides in a editDialog, this property holds it
142 * if it is so, the grid can't save records itsef, just update
143 * the editDialogs record property holding these records
145 * @cfg {Tine.widgets.dialog.EditDialog} editDialog
150 * if this resides in an editDialog, this property defines the
151 * property of the record of the editDialog, holding these records
153 * @type {String} editDialogRecordProperty
155 editDialogRecordProperty: null,
158 * config passed to edit dialog to open from this grid
160 * @cfg {Object} editDialogConfig
162 editDialogConfig: null,
165 * the edit dialog class to open from this grid
167 * @cfg {String} editDialogClass
169 editDialogClass: null,
172 * @cfg {String} i18nEmptyText
177 * @cfg {String} newRecordIcon
178 * icon for adding new records button
183 * @cfg {Boolean} i18nDeleteRecordAction
184 * update details panel if context menu is shown
186 updateDetailsPanelOnCtxMenu: true,
189 * @cfg {Number} autoRefreshInterval (seconds)
191 autoRefreshInterval: 300,
194 * @cfg {Boolean} hasFavoritesPanel
196 hasFavoritesPanel: true,
199 * @cfg {Boolean} hasQuickSearchFilterToolbarPlugin
201 hasQuickSearchFilterToolbarPlugin: true,
204 * disable 'select all pages' in paging toolbar
205 * @cfg {Boolean} disableSelectAllPages
207 disableSelectAllPages: false,
210 * enable if records should be multiple editable
211 * @cfg {Boolean} multipleEdit
216 * set if multiple edit requires special right
217 * @type {String} multipleEditRequiredRight
219 multipleEditRequiredRight: null,
222 * enable if selection of 2 records should allow merging
223 * @cfg {Boolean} duplicateResolvable
225 duplicateResolvable: false,
228 * @property autoRefreshTask
229 * @type Ext.util.DelayedTask
231 autoRefreshTask: null,
235 * @property updateOnSelectionChange
237 updateOnSelectionChange: true,
241 * @property copyEditAction
243 * TODO activate this by default
245 copyEditAction: false,
248 * @cfg {Boolean} moveAction
249 * activate moveAction
254 * disable delete confirmation by default
257 * @property disableDeleteConfirmation
259 disableDeleteConfirmation: false,
263 * @property actionToolbar
268 * @type Ext.ux.grid.PagingToolbar
269 * @property pagingToolbar
275 * @property contextMenu
280 * @property lastStoreTransactionId
283 lastStoreTransactionId: null,
286 * @property editBuffer - array of ids of records edited since last explicit refresh
292 * @property deleteQueue - array of ids of records currently being deleted
298 * configuration object of model from application starter
304 * group grid by this property
311 * header template for the grouping view, if needed
318 * @property selectionModel
319 * @type Tine.widgets.grid.FilterSelectionModel
321 selectionModel: null,
324 * add records from other applications using the split add button
325 * - activated by default
328 * @property splitAddButton
330 splitAddButton: true,
334 * do initial load (by loading default favorite) after render
338 initialLoadAfterRender: true,
341 * add "create new record" button
344 * @property addButton
355 * Makes the grid readonly, this means, no dialogs, no actions, nothing else than selection, no dbclick
360 * extend standard initComponent chain
364 initComponent: function(){
365 // init some translations
366 this.i18nRecordName = this.i18nRecordName ? this.i18nRecordName : this.recordClass.getRecordName();
367 this.i18nRecordsName = this.i18nRecordsName ? this.i18nRecordsName : this.recordClass.getRecordsName();
368 this.i18nContainerName = this.i18nContainerName ? this.i18nContainerName : this.recordClass.getContainerName();
369 this.i18nContainersName = this.i18nContainersName ? this.i18nContainersName : this.recordClass.getContainersName();
371 this.i18nEmptyText = this.i18nEmptyText ||
372 this.i18nContainersName
373 ? String.format(i18n._("There could not be found any {0}. Please try to change your filter-criteria, view-options or the {1} you search in."), this.i18nRecordsName, (this.i18nContainersName ? this.i18nContainersName : this.i18nRecordsName))
374 : String.format(i18n._("There could not be found any {0}. Please try to change your filter-criteria, view-options or change the module you search in."), this.i18nRecordsName);
376 this.i18nEditActionText = this.i18nEditActionText ? this.i18nEditActionText : [String.format(i18n.ngettext('Edit {0}', 'Edit {0}', 1), this.i18nRecordName), String.format(i18n.ngettext('Edit {0}', 'Edit {0}', 2), this.i18nRecordsName)];
378 this.editDialogConfig = this.editDialogConfig || {};
379 this.editBuffer = [];
380 this.deleteQueue = [];
382 // init generic stuff
383 if (this.modelConfig) {
387 this.initFilterPanel();
396 this.actionUpdater = new Tine.widgets.ActionUpdater({
397 evalGrants: this.evalGrants
400 if (!this.readOnly) {
406 // for some reason IE looses split height when outer layout is layouted
407 if (Ext.isIE6 || Ext.isIE7) {
408 this.on('show', function() {
409 if (this.layout.rendered && this.detailsPanel) {
410 var height = this.detailsPanel.getSize().height;
411 this.layout.south.split.setCurrentSize(height);
416 if (this.detailsPanel) {
417 this.on('resize', this.onContentResize, this, {buffer: 100});
420 if (this.listenMessageBus) {
421 this.initMessageBus();
424 Tine.widgets.grid.GridPanel.superclass.initComponent.call(this);
427 initMessageBus: function() {
429 channel: "recordchange",
430 topic: [this.recordClass.getMeta('appName'), this.recordClass.getMeta('modelName'), '*'].join('.'),
431 callback: this.onRecordChanges.createDelegate(this)
436 * bus notified about record changes
438 onRecordChanges: function(data, e) {
439 var existingRecord = this.store.getById(data.id);
440 if (existingRecord && e.topic.match(/\.update/)) {
441 // NOTE: local mode saves again (and again...)
442 this.onUpdateRecord(JSON.stringify(data)/*, 'local'*/);
443 } if (existingRecord && e.topic.match(/\.delete/)) {
444 this.store.remove(existingRecord);
446 // we can't evaluate the filters on client side to check compute if this affects us
447 // so just lets reload
449 removeStrategy: 'keepBuffered'
455 * returns canonical path part
458 getCanonicalPathSegment: function () {
459 var pathSegment = '';
460 if (this.canonicalName) {
461 // simple segment e.g. when used in a dialog
462 pathSegment = this.canonicalName;
463 } else if (this.recordClass) {
465 pathSegment = [this.recordClass.getMeta('modelName'), 'Grid'].join(Tine.Tinebase.CanonicalPath.separator);
471 onContentResize: function() {
472 // make sure details panel doesn't hide grid
473 if (this.detailsPanel) {
474 var gridHeight = this.grid.getHeight(),
475 detailsHeight = this.detailsPanel.getHeight();
477 if (detailsHeight/2 > gridHeight) {
478 var newDetailsHeight = this.getHeight() *.4;
479 this.layout.south.panel.setHeight(newDetailsHeight);
486 * initializes generic stuff when used with ModelConfiguration
488 initGeneric: function() {
489 if (this.modelConfig) {
491 Tine.log.debug('init generic gridpanel with config:');
492 Tine.log.debug(this.modelConfig);
494 // TODO move to uiConfig
495 if (this.modelConfig.hasOwnProperty('multipleEdit') && (this.modelConfig.multipleEdit === true)) {
496 this.multipleEdit = true;
497 this.multipleEditRequiredRight = (this.modelConfig.hasOwnProperty('multipleEditRequiredRight'))
498 ? this.modelConfig.multipleEditRequiredRight
502 // TODO move to uiConfig
503 if (this.modelConfig.hasOwnProperty('copyEditAction') && (this.modelConfig.copyEditAction === true)) {
504 this.copyEditAction = true;
508 // init generic columnModel
509 this.initGenericColumnModel();
513 * initialises the filter panel
515 * @param {Object} config
517 initFilterPanel: function(config) {
518 if (! this.filterToolbar && ! this.editDialog) {
519 var filterModels = [];
520 if (this.modelConfig) {
521 filterModels = this.getCustomfieldFilters();
522 } else if (Ext.isFunction(this.recordClass.getFilterModel)) {
523 filterModels = this.recordClass.getFilterModel().concat(this.getCustomfieldFilters());
525 this.filterToolbar = new Tine.widgets.grid.FilterPanel(Ext.apply({}, {
527 recordClass: this.recordClass,
529 filterModels: filterModels,
530 defaultFilter: this.recordClass.getMeta('defaultFilter') ? this.recordClass.getMeta('defaultFilter') : 'query',
531 filters: this.defaultFilters || []
534 this.plugins = this.plugins || [];
535 this.plugins.push(this.filterToolbar);
540 * initializes the generic column model on auto bootstrap
542 initGenericColumnModel: function() {
543 if (this.modelConfig) {
545 Ext.each(this.modelConfig.fieldKeys, function(key) {
546 var fieldConfig = this.modelConfig.fields[key];
547 globalI18n = (fieldConfig && fieldConfig.hasOwnProperty('useGlobalTranslation'));
549 if (fieldConfig.type === 'virtual') {
550 fieldConfig = fieldConfig.config;
553 // don't show multiple record fields
554 if (fieldConfig.type == 'records') {
558 // don't show parent property in dependency of an editDialog
559 if (this.editDialog && fieldConfig.hasOwnProperty('config') && fieldConfig.config.isParent) {
563 // don't show record field if the user doesn't have the right on the application
564 if (fieldConfig.type == 'record' && !(fieldConfig.config && fieldConfig.config.doNotCheckModuleRight) && (! Tine.Tinebase.common.hasRight('view', fieldConfig.config.appName, fieldConfig.config.modelName.toLowerCase() + 's'))) {
568 // If no label exists, don't use in grid
569 if (fieldConfig.label) {
572 dataIndex: (fieldConfig.type == 'relation') ? 'relations' : key,
573 header: globalI18n ? i18n._(fieldConfig.label) : this.app.i18n._(fieldConfig.label),
574 hidden: fieldConfig.hasOwnProperty('shy') ? fieldConfig.shy : false, // defaults to false
575 sortable: (fieldConfig.hasOwnProperty('sortable') && fieldConfig.sortable == false) ? false : true // defaults to true
578 if (fieldConfig.hasOwnProperty('summaryType')) {
579 config.summaryType = fieldConfig.summaryType;
582 var renderer = Tine.widgets.grid.RendererManager.get(this.app.name, this.recordClass.getMeta('modelName'), key);
584 config.renderer = renderer;
586 columns.push(config);
590 if (this.modelConfig.hasCustomFields) {
591 columns = columns.concat(this.getCustomfieldColumns());
594 columns = columns.concat(this.getCustomColumns());
596 this.gridConfig.cm = new Ext.grid.ColumnModel({
606 * template method to allow adding custom columns
610 getCustomColumns: function() {
617 * NOTE: Order of items matters! Ext.Layout.Border.SplitRegion.layout() does not
618 * fence the rendering correctly, as such it's impotant, so have the ftb
619 * defined after all other layout items
621 initLayout: function() {
627 tbar: this.pagingToolbar,
633 if (this.detailsPanel) {
635 // just in case it's a config only
636 this.detailsPanel = Ext.ComponentMgr.create(this.detailsPanel);
642 collapseMode: 'mini',
646 height: this.detailsPanel.defaultHeight ? this.detailsPanel.defaultHeight : 125,
647 items: this.detailsPanel,
648 canonicalName: 'DetailsPanel'
651 this.detailsPanel.doBind(this.grid);
654 // add filter toolbar
655 if (this.filterToolbar) {
660 items: this.filterToolbar,
663 afterlayout: function(ct) {
665 ct.setHeight(Math.min(120, this.filterToolbar.getHeight() + (ct.topToolbar ? ct.topToolbar.getHeight() : 0)));
666 ct.getEl().child('div[class^="x-panel-body"]', true).scrollTop = 1000000;
667 ct.ownerCt.layout.layout();
677 * init actions with actionToolbar, contextMenu and actionUpdater
681 initActions: function() {
682 this.newRecordIcon = this.newRecordIcon!== null ? this.newRecordIcon : this.app.appName + 'IconCls';
683 if (! Ext.util.CSS.getRule('.' + this.newRecordIcon)) {
684 this.newRecordIcon = 'ApplicationIconCls';
687 var services = Tine.Tinebase.registry.get('serviceMap').services;
689 this.action_editInNewWindow = new Ext.Action({
690 requiredGrant: 'readGrant',
691 requiredMultipleGrant: 'editGrant',
692 requiredMultipleRight: this.multipleEditRequiredRight,
693 text: this.i18nEditActionText ? this.i18nEditActionText[0] : String.format(i18n._('Edit {0}'), this.i18nRecordName),
694 singularText: this.i18nEditActionText ? this.i18nEditActionText[0] : String.format(i18n._('Edit {0}'), this.i18nRecordName),
695 pluralText: this.i18nEditActionText ? this.i18nEditActionText[1] : String.format(i18n.ngettext('Edit {0}', 'Edit {0}', 1), this.i18nRecordsName),
697 translationObject: this.i18nEditActionText ? this.app.i18n : i18n,
699 handler: this.onEditInNewWindow.createDelegate(this, [{actionType: 'edit'}]),
700 iconCls: 'action_edit',
702 allowMultiple: this.multipleEdit
705 this.action_editCopyInNewWindow = new Ext.Action({
706 hidden: ! this.copyEditAction,
707 requiredGrant: 'readGrant',
708 text: String.format(i18n._('Copy {0}'), this.i18nRecordName),
711 handler: this.onEditInNewWindow.createDelegate(this, [{actionType: 'copy'}]),
712 iconCls: 'action_editcopy',
716 this.action_addInNewWindow = (this.addButton) ? new Ext.Action({
717 requiredGrant: 'addGrant',
719 text: this.i18nAddActionText ? this.app.i18n._hidden(this.i18nAddActionText) : String.format(i18n._('Add {0}'), this.i18nRecordName),
720 handler: this.onEditInNewWindow.createDelegate(this, [{actionType: 'add'}]),
721 iconCls: this.newRecordIcon,
725 this.actions_print = new Ext.Action({
726 requiredGrant: 'readGrant',
727 text: i18n._('Print Page'),
729 handler: function() {
730 Ext.ux.Printer.print(this.getGrid());
732 iconCls: 'action_print',
737 this.initDeleteAction(services);
739 this.action_move = new Ext.Action({
740 requiredGrant: 'editGrant',
741 requiredMultipleGrant: 'editGrant',
742 requiredMultipleRight: this.multipleEditRequiredRight,
743 singularText: this.i18nMoveActionText ? this.i18nMoveActionText[0] : String.format(i18n.ngettext('Move {0}', 'Move {0}', 1), this.i18nRecordName),
744 pluralText: this.i18nMoveActionText ? this.i18nMoveActionText[1] : String.format(i18n.ngettext('Move {0}', 'Move {0}', 1), this.i18nRecordsName),
745 translationObject: this.i18nMoveActionText ? this.app.i18n : i18n,
746 text: this.i18nMoveActionText ? this.i18nMoveActionText[0] : String.format(i18n.ngettext('Move {0}', 'Move {0}', 1), this.i18nRecordName),
748 hidden: !this.moveAction || !this.recordClass.getMeta('containerProperty'),
750 handler: this.onMoveRecords,
752 iconCls: 'action_move',
753 allowMultiple: this.multipleEdit
757 this.action_tagsMassAttach = new Tine.widgets.tags.TagsMassAttachAction({
758 hidden: ! this.recordClass.getField('tags'),
759 selectionModel: this.grid.getSelectionModel(),
760 recordClass: this.recordClass,
761 updateHandler: this.loadGridData.createDelegate(this),
765 this.action_tagsMassDetach = new Tine.widgets.tags.TagsMassDetachAction({
766 hidden: ! this.recordClass.getField('tags'),
767 selectionModel: this.grid.getSelectionModel(),
768 recordClass: this.recordClass,
769 updateHandler: this.loadGridData.createDelegate(this),
773 this.action_resolveDuplicates = new Ext.Action({
775 text: String.format(i18n._('Merge {0}'), this.i18nRecordsName),
776 iconCls: 'action_resolveDuplicates',
778 handler: this.onResolveDuplicates,
780 actionUpdater: function(action, grants, records) {
781 if (records && (records.length != 2)) action.setDisabled(true);
782 else action.setDisabled(false);
789 // add actions to updater
790 this.actionUpdater.addActions([
791 this.action_addInNewWindow,
792 this.action_editInNewWindow,
794 this.action_editCopyInNewWindow,
795 this.action_deleteRecord,
796 this.action_tagsMassAttach,
797 this.action_tagsMassDetach,
798 this.action_resolveDuplicates
801 // init actionToolbar (needed for correct fitertoolbar init atm -> fixme)
802 this.getActionToolbar();
805 initExports: function() {
806 if (this.actions_export !== undefined) return;
808 var _ = window.lodash,
809 exportFunction = this.app.name + '.export' + this.recordClass.getMeta('modelName') + 's',
810 additionalItems = [];
812 // create items from available export formats (depricated -> use definitions)
813 if (this.modelConfig && this.modelConfig['export']) {
814 if (this.modelConfig['export'].supportedFormats) {
815 Ext.each(this.modelConfig['export'].supportedFormats, function (format, i) {
816 additionalItems.unshift(new Tine.widgets.grid.ExportButton({
817 // TODO format toUpper
818 text: String.format(i18n._('Export as {0}'), format),
820 iconCls: 'tinebase-action-export-' + format,
821 exportFunction: exportFunction,
829 // exports from export definitions
830 this.actions_export = Tine.widgets.exportAction.getExportButton(this.recordClass, {
831 exportFunction: exportFunction,
833 }, Tine.widgets.exportAction.SCOPE_MULTI, additionalItems);
835 if (this.actions_export) {
836 this.actionUpdater.addActions([this.actions_export]);
840 initImports: function() {
841 if (this.actions_import !== undefined) return;
843 if (this.modelConfig && this.modelConfig['import']) {
844 this.actions_import = new Ext.Action({
845 requiredGrant: 'addGrant',
846 text: i18n._('Import items'),
848 handler: this.onImport,
849 iconCls: 'action_import',
854 this.actionUpdater.addActions([this.actions_import]);
859 * import inventory items
861 * @param {Button} btn
863 onImport: function(btn) {
864 var treePanel = this.treePanel || this.app.getMainScreen().getWestPanel().getContainerTreePanel(),
867 var container = _.get(this.modelConfig, 'import.defaultImportContainerRegistryKey', false);
870 container = treePanel.getDefaultContainer(container);
873 Tine.widgets.dialog.ImportDialog.openWindow({
874 appName: this.app.name,
875 modelName: this.recordClass.getMeta('modelName'),
876 defaultImportContainer: container,
879 'finish': function() {
881 preserveCursor: false,
882 preserveSelection: false,
883 preserveScroller: false,
884 removeStrategy: 'default'
892 * initializes the delete action
894 * @param {Object} services the rpc service map from the registry
896 initDeleteAction: function(services) {
897 // note: unprecise plural form here, but this is hard to change
898 this.action_deleteRecord = new Ext.Action({
899 requiredGrant: 'deleteGrant',
901 singularText: this.i18nDeleteActionText ? this.i18nDeleteActionText[0] : String.format(i18n.ngettext('Delete {0}', 'Delete {0}', 1), this.i18nRecordName),
902 pluralText: this.i18nDeleteActionText ? this.i18nDeleteActionText[1] : String.format(i18n.ngettext('Delete {0}', 'Delete {0}', 1), this.i18nRecordsName),
903 translationObject: this.i18nDeleteActionText ? this.app.i18n : i18n,
904 text: this.i18nDeleteActionText ? this.i18nDeleteActionText[0] : String.format(i18n.ngettext('Delete {0}', 'Delete {0}', 1), this.i18nRecordName),
905 handler: this.onDeleteRecords,
907 iconCls: 'action_delete',
910 // if nested in a editDialog (dependent record), the service won't exist
911 if (! this.editDialog) {
912 this.disableDeleteActionCheckServiceMap(services);
917 * disable delete action if no delete method was found in serviceMap
919 * @param {Object} services the rpc service map from the registry
921 * TODO this should be configurable as not all grids use remote delete
923 disableDeleteActionCheckServiceMap: function(services) {
925 var serviceKey = this.app.name + '.delete' + this.recordClass.getMeta('modelName') + 's';
926 if (! services.hasOwnProperty(serviceKey)) {
927 this.action_deleteRecord.setDisabled(1);
928 this.action_deleteRecord.initialConfig.actionUpdater = function(action) {
929 Tine.log.debug("disable delete action because no delete method was found in serviceMap");
930 action.setDisabled(1);
940 initStore: function() {
942 // store is already initialized
946 if (this.recordProxy) {
947 var storeClass = this.groupField ? Ext.data.GroupingStore : Ext.data.Store;
948 this.store = new storeClass({
949 fields: this.recordClass,
950 proxy: this.recordProxy,
951 reader: this.recordProxy.getReader(),
952 remoteSort: this.storeRemoteSort,
953 sortInfo: this.defaultSortInfo,
957 'add': this.onStoreAdd,
958 'remove': this.onStoreRemove,
959 'update': this.onStoreUpdate,
960 'beforeload': this.onStoreBeforeload,
961 'load': this.onStoreLoad,
962 'beforeloadrecords': this.onStoreBeforeLoadRecords,
963 'loadexception': this.onStoreLoadException
967 this.store = new Tine.Tinebase.data.RecordStore({
968 recordClass: this.recordClass
973 this.autoRefreshTask = new Ext.util.DelayedTask(this.loadGridData, this, [{
974 removeStrategy: 'keepBuffered',
980 * returns view row class
982 getViewRowClass: function(record, index, rowParams, store) {
983 var noLongerInFilter = record.not_in_filter;
986 if (noLongerInFilter) {
987 className += 'tine-grid-row-nolongerinfilter';
993 * new entry event -> add new record to store
995 * @param {Object} recordData
998 onStoreNewEntry: function(recordData) {
999 var initialData = null;
1000 if (Ext.isFunction(this.recordClass.getDefaultData)) {
1001 initialData = Ext.apply(this.recordClass.getDefaultData(), recordData);
1003 initialData = recordData;
1005 var record = new this.recordClass(initialData);
1006 this.store.insert(0 , [record]);
1008 if (this.usePagingToolbar) {
1009 this.pagingToolbar.refresh.disable();
1011 this.recordProxy.saveRecord(record, {
1013 success: function(newRecord) {
1014 this.store.suspendEvents();
1015 this.store.remove(record);
1016 this.store.insert(0 , [newRecord]);
1017 this.store.resumeEvents();
1019 this.addToEditBuffer(newRecord);
1022 removeStrategy: 'keepBuffered'
1033 * @param {Object} grid
1034 * @param {Number} colIdx
1038 onHeaderClick: function(grid, colIdx, e) {
1040 Ext.apply(this.store.lastOptions, {
1041 preserveCursor: true,
1042 preserveSelection: true,
1043 preserveScroller: true,
1044 removeStrategy: 'default'
1049 * called when Records have been added to the Store
1051 onStoreAdd: function(store, records, index) {
1052 this.store.totalLength += records.length;
1053 if (this.pagingToolbar) {
1054 this.pagingToolbar.updateInfo();
1059 * called when a Record has been removed from the Store
1061 onStoreRemove: function(store, record, index) {
1062 this.store.totalLength--;
1063 if (this.pagingToolbar) {
1064 this.pagingToolbar.updateInfo();
1069 * called when the store gets updated, e.g. from editgrid
1071 * @param {Ext.data.store} store
1072 * @param {Tine.Tinebase.data.Record} record
1073 * @param {String} operation
1075 onStoreUpdate: function(store, record, operation) {
1076 switch (operation) {
1077 case Ext.data.Record.EDIT:
1078 this.addToEditBuffer(record);
1080 if (this.usePagingToolbar) {
1081 this.pagingToolbar.refresh.disable();
1083 // don't save these records. Add them to the parents' record store
1084 if (this.editDialog) {
1086 store.each(function(item) {
1087 items.push(item.data);
1090 this.editDialog.record.set(this.editDialogRecordProperty, items);
1091 this.editDialog.fireEvent('updateDependent');
1092 } else if (this.recordProxy) {
1093 this.recordProxy.saveRecord(record, {
1095 success: function(updatedRecord) {
1096 store.commitChanges();
1098 // update record in store to prevent concurrency problems
1099 record.data = updatedRecord.data;
1102 removeStrategy: 'keepBuffered'
1108 case Ext.data.Record.COMMIT:
1109 //nothing to do, as we need to reload the store anyway.
1115 * called before store queries for data
1117 onStoreBeforeload: function(store, options) {
1119 // define a transaction
1120 this.lastStoreTransactionId = options.transactionId = Ext.id();
1122 options.params = options.params || {};
1123 // always start with an empty filter set!
1124 // this is important for paging and sort header!
1125 options.params.filter = [];
1127 if (! options.removeStrategy || options.removeStrategy !== 'keepBuffered') {
1128 this.editBuffer = [];
1131 // options.preserveSelection = options.hasOwnProperty('preserveSelection') ? options.preserveSelection : true;
1132 // options.preserveScroller = options.hasOwnProperty('preserveScroller') ? options.preserveScroller : true;
1134 // fix nasty paging tb
1135 Ext.applyIf(options.params, this.defaultPaging);
1139 * called after a new set of Records has been loaded
1141 * @param {Ext.data.Store} this.store
1142 * @param {Array} loaded records
1143 * @param {Array} load options
1146 onStoreLoad: function(store, records, options) {
1147 // we always focus the first row so that keynav starts in the grid
1148 // this resets scroller ;-( -> need a better solution
1149 // if (this.store.getCount() > 0) {
1150 // this.grid.getView().focusRow(0);
1153 // restore selection
1154 if (Ext.isArray(options.preserveSelection)) {
1155 Ext.each(options.preserveSelection, function(record) {
1156 var row = this.store.indexOfId(record.id);
1158 this.grid.getSelectionModel().selectRow(row, true);
1164 if (Ext.isNumber(options.preserveScroller)) {
1165 this.grid.getView().scroller.dom.scrollTop = options.preserveScroller;
1168 // reset autoRefresh
1169 if (window.isMainWindow && this.autoRefreshInterval) {
1170 this.autoRefreshTask.delay(this.autoRefreshInterval * 1000);
1175 * on store load exception
1177 * @param {Tine.Tinebase.data.RecordProxy} proxy
1178 * @param {String} type
1179 * @param {Object} error
1180 * @param {Object} options
1182 onStoreLoadException: function(proxy, type, error, options) {
1184 // reset autoRefresh
1185 if (window.isMainWindow && this.autoRefreshInterval) {
1186 this.autoRefreshTask.delay(this.autoRefreshInterval * 5000);
1189 if (this.usePagingToolbar && this.pagingToolbar.refresh) {
1190 this.pagingToolbar.refresh.enable();
1193 if (! options.autoRefresh) {
1194 proxy.handleRequestException(error);
1196 Tine.log.debug('Tine.widgets.grid.GridPanel::onStoreLoadException -> auto refresh failed.');
1201 * onStoreBeforeLoadRecords
1204 * @param {Object} options
1205 * @param {Boolean} success
1206 * @param {Ext.data.Store} store
1208 onStoreBeforeLoadRecords: function(o, options, success, store) {
1210 if (this.lastStoreTransactionId && options.transactionId && this.lastStoreTransactionId !== options.transactionId) {
1211 Tine.log.debug('onStoreBeforeLoadRecords - cancelling old transaction request.');
1215 // save selection -> will be applied onLoad
1216 if (options.preserveSelection) {
1217 options.preserveSelection = this.grid.getSelectionModel().getSelections();
1220 // save scroller -> will be applied onLoad
1221 if (options.preserveScroller && this.grid.getView().scroller && this.grid.getView().scroller.dom) options.preserveScroller = this.grid.getView().scroller.dom.scrollTop;
1223 // apply removeStrategy
1224 if (! options.removeStrategy || options.removeStrategy === 'default') {
1230 recordToLoadCollection = new Ext.util.MixedCollection();
1232 // fill new collection
1233 Ext.each(o.records, function(record) {
1234 recordToLoadCollection.add(record.id, record);
1237 // assemble update & keep
1238 this.store.each(function(currentRecord) {
1239 var recordToLoad = recordToLoadCollection.get(currentRecord.id);
1241 // we replace records that are the same, because otherwise this would not work for local changes
1242 if (recordToLoad.isObsoletedBy(currentRecord)) {
1243 records.push(currentRecord);
1244 recordsIds.push(currentRecord.id);
1246 records.push(recordToLoad);
1247 recordsIds.push(recordToLoad.id);
1249 } else if (options.removeStrategy === 'keepAll' || (options.removeStrategy === 'keepBuffered' && this.editBuffer.indexOf(currentRecord.id) >= 0)) {
1250 var copiedRecord = currentRecord.copy();
1251 copiedRecord.not_in_filter = true;
1252 records.push(copiedRecord);
1253 recordsIds.push(currentRecord.id);
1258 recordToLoadCollection.each(function(record, idx) {
1259 if (recordsIds.indexOf(record.id) == -1 && this.deleteQueue.indexOf(record.id) == -1) {
1260 var lastRecord = recordToLoadCollection.itemAt(idx-1);
1261 var lastRecordIdx = lastRecord ? recordsIds.indexOf(lastRecord.id) : -1;
1262 records.splice(lastRecordIdx+1, 0, record);
1263 recordsIds.splice(lastRecordIdx+1, 0, record.id);
1267 o.records = records;
1269 // hide current records from store.loadRecords()
1270 // @see 0008210: email grid: set flag does not work sometimes
1271 this.store.clearData();
1275 * perform the initial load of grid data
1277 initialLoad: function() {
1278 var defaultFavorite = Tine.widgets.persistentfilter.model.PersistentFilter.getDefaultFavorite(
1279 this.app.appName, this.recordClass.prototype.modelName
1281 favoritesPanel = this.app.getMainScreen()
1282 && typeof this.app.getMainScreen().getWestPanel === 'function'
1283 && typeof this.app.getMainScreen().getWestPanel().getFavoritesPanel === 'function'
1284 && this.hasFavoritesPanel
1285 ? this.app.getMainScreen().getWestPanel().getFavoritesPanel()
1288 if (defaultFavorite && favoritesPanel) {
1289 favoritesPanel.selectFilter(defaultFavorite);
1291 if (! this.editDialog) {
1292 this.store.load.defer(10, this.store, [ typeof this.autoLoad == 'object' ? this.autoLoad : undefined]);
1294 // editDialog exists, so get the records from there.
1295 var items = this.editDialog.record.get(this.editDialogRecordProperty);
1296 if (Ext.isArray(items)) {
1297 Ext.each(items, function(item) {
1298 var record = this.recordProxy.recordReader({responseText: Ext.encode(item)});
1299 this.store.addSorted(record);
1305 if (this.usePagingToolbar && this.recordProxy) {
1306 this.pagingToolbar.refresh.disable.defer(10, this.pagingToolbar.refresh);
1311 * init ext grid panel
1314 initGrid: function() {
1315 var preferences = Tine.Tinebase.registry.get('preferences');
1318 this.gridConfig = Ext.applyIf(this.gridConfig || {}, {
1319 stripeRows: preferences.get('gridStripeRows') ? preferences.get('gridStripeRows') : false,
1320 loadMask: preferences.get('gridLoadMask') ? preferences.get('gridLoadMask') : false
1323 // added paging number of result read from settings
1324 if (preferences.get('pageSize') != null) {
1325 this.defaultPaging = {
1327 limit: parseInt(preferences.get('pageSize'), 10)
1332 // generic empty text
1333 this.i18nEmptyText = i18n.gettext('No data to display');
1336 this.selectionModel = new Tine.widgets.grid.FilterSelectionModel({
1340 this.selectionModel.on('selectionchange', function(sm) {
1341 this.actionUpdater.updateActions(sm);
1343 this.ctxNode = this.selectionModel.getSelections();
1344 if (this.updateOnSelectionChange && this.detailsPanel) {
1345 this.detailsPanel.onDetailsUpdate(sm);
1349 if (this.usePagingToolbar) {
1350 this.pagingToolbar = new Ext.ux.grid.PagingToolbar(Ext.apply({
1351 pageSize: this.defaultPaging && this.defaultPaging.limit ? this.defaultPaging.limit : 50,
1354 displayMsg: i18n._('Displaying records {0} - {1} of {2}').replace(/records/, this.i18nRecordsName),
1355 emptyMsg: String.format(i18n._("No {0} to display"), this.i18nRecordsName),
1356 displaySelectionHelper: true,
1357 sm: this.selectionModel,
1358 disableSelectAllPages: this.disableSelectAllPages,
1359 nested: this.editDialog ? true : false
1360 }, this.pagingConfig));
1361 // mark next grid refresh as paging-refresh
1362 this.pagingToolbar.on('beforechange', function() {
1363 this.grid.getView().isPagingRefresh = true;
1367 // which grid to use?
1368 var Grid = this.gridConfig.quickaddMandatory ? Ext.ux.grid.QuickaddGridPanel : (this.gridConfig.gridType || Ext.grid.GridPanel);
1370 this.gridConfig.store = this.store;
1372 // activate grid header menu for column selection
1373 this.gridConfig.plugins = this.gridConfig.plugins ? this.gridConfig.plugins : [];
1374 this.gridConfig.plugins.push(new Ext.ux.grid.GridViewMenuPlugin({}));
1375 this.gridConfig.enableHdMenu = false;
1377 if (this.stateful) {
1378 this.gridConfig.stateful = true;
1379 this.gridConfig.stateId = this.stateId + '-Grid' + this.stateIdSuffix;
1382 this.grid = new Grid(Ext.applyIf(this.gridConfig, {
1385 sm: this.selectionModel,
1386 view: this.createView()
1389 // init various grid / sm listeners
1390 this.grid.on('keydown', this.onKeyDown, this);
1391 this.grid.on('rowclick', this.onRowClick, this);
1392 this.grid.on('rowdblclick', this.onRowDblClick, this);
1393 this.grid.on('newentry', this.onStoreNewEntry, this);
1394 this.grid.on('headerclick', this.onHeaderClick, this);
1396 this.grid.on('rowcontextmenu', this.onRowContextMenu, this);
1401 * creates and returns the view for the grid
1403 * @return {Ext.grid.GridView}
1405 createView: function() {
1408 if (this.groupField && ! this.groupTextTpl) {
1409 this.groupTextTpl = '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "' + i18n._("Records") + '" : "' + i18n._("Record") + '"]})';
1412 var viewClass = this.groupField ? Ext.grid.GroupingView : Ext.grid.GridView;
1413 var view = new viewClass({
1414 getRowClass: this.getViewRowClass,
1418 emptyText: this.i18nEmptyText,
1419 groupTextTpl: this.groupTextTpl,
1420 onLoad: Ext.grid.GridView.prototype.onLoad.createInterceptor(function() {
1421 if (this.grid.getView().isPagingRefresh) {
1422 this.grid.getView().isPagingRefresh = false;
1434 * executed after outer panel rendering process
1436 afterRender: function() {
1437 Tine.widgets.grid.GridPanel.superclass.afterRender.apply(this, arguments);
1438 if (this.initialLoadAfterRender) {
1444 * trigger store load with grid related options
1446 * TODO rethink -> preserveCursor and preserveSelection might conflict on page breaks!
1447 * TODO don't reload details panel when selection is preserved
1449 * @param {Object} options
1451 loadGridData: function(options) {
1452 var options = options || {};
1454 Ext.applyIf(options, {
1455 callback: Ext.emptyFn,
1459 preserveCursor: true,
1460 preserveSelection: true,
1461 preserveScroller: true,
1462 removeStrategy: 'default'
1465 if (options.preserveCursor && this.usePagingToolbar) {
1466 options.params.start = this.pagingToolbar.cursor;
1469 this.store.load(options);
1473 * get action toolbar
1475 * @return {Ext.Toolbar}
1477 getActionToolbar: function() {
1478 if (! this.actionToolbar) {
1479 var additionalItems = this.getActionToolbarItems();
1483 if (this.action_addInNewWindow) {
1484 if (this.splitAddButton) {
1485 items.push(Ext.apply(
1486 new Ext.SplitButton(this.action_addInNewWindow), {
1491 menu: new Ext.menu.Menu({
1494 ptype: 'ux.itemregistry',
1495 key: 'Tine.widgets.grid.GridPanel.addButton'
1501 items.push(Ext.apply(
1502 new Ext.Button(this.action_addInNewWindow), {
1511 if (this.action_editInNewWindow) {
1512 items.push(Ext.apply(
1513 new Ext.Button(this.action_editInNewWindow), {
1521 if (this.action_deleteRecord) {
1522 items.push(Ext.apply(
1523 new Ext.Button(this.action_deleteRecord), {
1531 if (this.actions_print) {
1532 items.push(Ext.apply(
1533 new (this.actions_print.initialConfig && this.actions_print.initialConfig.menu ? Ext.SplitButton : Ext.Button) (this.actions_print), {
1541 var importExportButtons = [];
1543 if (this.actions_export) {
1544 importExportButtons.push(Ext.apply(new Ext.Button(this.actions_export), {
1550 if (this.actions_import) {
1551 importExportButtons.push(Ext.apply(new Ext.Button(this.actions_import), {
1558 if (importExportButtons.length > 0) {
1560 xtype: 'buttongroup',
1563 items: importExportButtons
1567 this.actionToolbar = new Ext.Toolbar({
1568 canonicalName: [this.recordClass.getMeta('modelName'), 'ActionToolbar'].join(Tine.Tinebase.CanonicalPath.separator),
1570 xtype: 'buttongroup',
1572 buttonAlign: 'left',
1573 enableOverflow: true,
1575 ptype: 'ux.itemregistry',
1576 key: this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ActionToolbar-leftbtngrp'
1578 items: items.concat(Ext.isArray(additionalItems) ? additionalItems : [])
1579 }].concat(Ext.isArray(additionalItems) ? [] : [additionalItems])
1582 this.actionToolbar.on('resize', this.onActionToolbarResize, this, {buffer: 250});
1583 this.actionToolbar.on('show', this.onActionToolbarResize, this);
1585 if (this.filterToolbar && typeof this.filterToolbar.getQuickFilterField == 'function') {
1586 this.actionToolbar.add('->', this.filterToolbar.getQuickFilterField());
1590 return this.actionToolbar;
1593 onActionToolbarResize: function(tb) {
1594 if (! tb.rendered) return;
1595 var actionGrp = tb.items.get(0),
1596 availableWidth = tb.getBox()['width'] - 5,
1597 maxNeededWidth = Ext.layout.ToolbarLayout.prototype.triggerWidth + 10;
1599 tb.items.each(function(c, idx) {
1600 if (idx > 0 && !c.isFill) {
1601 availableWidth -= c.getPositionEl().dom.parentNode.offsetWidth;
1605 actionGrp.items.each(function(c) {
1606 maxNeededWidth += Ext.layout.ToolbarLayout.prototype.getItemWidth(c);
1609 actionGrp.setWidth(Math.min(availableWidth, maxNeededWidth));
1614 * template fn for subclasses to add custom items to action toolbar
1616 * @return {Array/Object}
1618 getActionToolbarItems: function() {
1619 var items = this.actionToolbarItems || [];
1621 if (! Ext.isEmpty(items)) {
1622 // legacy handling! subclasses should register all actions when initializing actions
1623 this.actionUpdater.addActions(items);
1630 * returns rows context menu
1632 * @param {Ext.grid.GridPanel} grid
1633 * @param {Number} row
1634 * @param {Ext.EventObject} e
1635 * @return {Ext.menu.Menu}
1637 getContextMenu: function(grid, row, e) {
1639 if (! this.contextMenu) {
1642 if (this.action_addInNewWindow) items.push(this.action_addInNewWindow);
1643 if (this.action_editInNewWindow) items.push(this.action_editInNewWindow);
1644 if (this.action_editCopyInNewWindow) items.push(this.action_editCopyInNewWindow);
1645 if (this.action_move) items.push(this.action_move);
1646 if (this.action_deleteRecord) items.push(this.action_deleteRecord);
1648 if (this.duplicateResolvable) {
1649 items.push(this.action_resolveDuplicates);
1652 if (this.actions_export) {
1653 items.push('-', this.actions_export);
1656 if (this.action_tagsMassAttach && ! this.action_tagsMassAttach.hidden) {
1657 items.push('-', this.action_tagsMassAttach, this.action_tagsMassDetach);
1660 // lookup additional items
1661 items = items.concat(this.getContextMenuItems());
1663 // New record of another app
1664 this.newRecordMenu = new Ext.menu.Menu({
1667 ptype: 'ux.itemregistry',
1668 key: this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ContextMenu-New'
1672 this.newRecordAction = new Ext.Action({
1673 text: i18n._('New...'),
1674 hidden: ! this.newRecordMenu.items.length,
1675 iconCls: this.app.getIconCls(),
1677 menu: this.newRecordMenu
1680 items.push(this.newRecordAction);
1682 // Add to record of another app
1683 this.addToRecordMenu = new Ext.menu.Menu({
1686 ptype: 'ux.itemregistry',
1687 key: this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ContextMenu-Add'
1691 this.addToRecordAction = new Ext.Action({
1692 text: i18n._('Add to...'),
1693 hidden: ! this.addToRecordMenu.items.length,
1694 iconCls: this.app.getIconCls(),
1696 menu: this.addToRecordMenu
1699 items.push(this.addToRecordAction);
1701 this.contextMenu = new Ext.menu.Menu({
1704 ptype: 'ux.itemregistry',
1705 key: this.app.appName + '-' + this.recordClass.prototype.modelName + '-GridPanel-ContextMenu'
1707 ptype: 'ux.itemregistry',
1708 key: 'Tinebase-MainContextMenu'
1713 return this.contextMenu;
1717 * template fn for subclasses to add custom items to context menu
1721 getContextMenuItems: function() {
1722 var items = this.contextMenuItems || [];
1724 if (! Ext.isEmpty(items)) {
1725 // legacy handling! subclasses should register all actions when initializing actions
1726 this.actionUpdater.addActions(items);
1733 * get modlog columns
1735 * shouldn' be used anymore
1736 * @TODO: use applicationstarter and modelconfiguration
1741 getModlogColumns: function() {
1743 { id: 'creation_time', header: i18n._('Creation Time'), dataIndex: 'creation_time', renderer: Tine.Tinebase.common.dateRenderer, hidden: true, sortable: true },
1744 { id: 'created_by', header: i18n._('Created By'), dataIndex: 'created_by', renderer: Tine.Tinebase.common.usernameRenderer, hidden: true, sortable: true },
1745 { id: 'last_modified_time', header: i18n._('Last Modified Time'), dataIndex: 'last_modified_time', renderer: Tine.Tinebase.common.dateRenderer, hidden: true, sortable: true },
1746 { id: 'last_modified_by', header: i18n._('Last Modified By'), dataIndex: 'last_modified_by', renderer: Tine.Tinebase.common.usernameRenderer, hidden: true, sortable: true }
1753 * get custom field columns for column model
1757 getCustomfieldColumns: function() {
1758 var modelName = this.recordClass.getMeta('appName') + '_Model_' + this.recordClass.getMeta('modelName'),
1759 cfConfigs = Tine.widgets.customfields.ConfigManager.getConfigs(this.app, modelName),
1762 Ext.each(cfConfigs, function(cfConfig) {
1765 header: cfConfig.get('definition').label,
1766 dataIndex: 'customfields',
1767 renderer: Tine.widgets.customfields.Renderer.get(this.app, cfConfig),
1777 * get custom field filter for filter toolbar
1781 getCustomfieldFilters: function() {
1782 var modelName = this.recordClass.getMeta('appName') + '_Model_' + this.recordClass.getMeta('modelName'),
1783 cfConfigs = Tine.widgets.customfields.ConfigManager.getConfigs(this.app, modelName),
1785 Ext.each(cfConfigs, function(cfConfig) {
1786 var cfDefinition = cfConfig.get('definition');
1787 switch (cfDefinition.type) {
1790 filtertype: 'foreignrecord',
1791 label: cfDefinition.label,
1793 ownRecordClass: this.recordClass,
1794 foreignRecordClass: eval(cfDefinition.recordConfig.value.records),
1795 linkType: 'foreignId',
1796 ownField: 'customfield:' + cfConfig.id
1800 // @TODO implement me
1803 result.push({filtertype: 'tinebase.customfield', app: this.app, cfConfig: cfConfig});
1812 * returns filter toolbar
1816 * TODO this seems to be legacy code that is only used in some apps (Calendar, Felamimail, ...)
1817 * -> should be removed
1818 * -> we use initFilterPanel() now
1820 getFilterToolbar: function(config) {
1821 config = config || {};
1822 return new Tine.widgets.grid.FilterPanel(Ext.apply(config, {
1824 recordClass: this.recordClass,
1825 filterModels: this.recordClass.getFilterModel().concat(this.getCustomfieldFilters()),
1826 defaultFilter: 'query',
1827 filters: this.defaultFilters || []
1832 * return store from grid
1834 * @return {Ext.data.Store}
1836 getStore: function() {
1837 return this.grid.getStore();
1841 * return view from grid
1843 * @return {Ext.grid.GridView}
1845 getView: function() {
1846 return this.grid.getView();
1852 * @return {Ext.ux.grid.QuickaddGridPanel|Ext.grid.GridPanel}
1854 getGrid: function() {
1862 onKeyDown: function(e){
1864 switch (e.getKey()) {
1866 // select only current page
1867 this.grid.getSelectionModel().selectAll(true);
1871 if (this.action_editInNewWindow && !this.action_editInNewWindow.isDisabled()) {
1872 this.onEditInNewWindow.call(this, {
1879 if (this.action_addInNewWindow && !this.action_addInNewWindow.isDisabled()) {
1880 this.onEditInNewWindow.call(this, {
1887 if (this.filterToolbar && this.hasQuickSearchFilterToolbarPlugin) {
1889 this.filterToolbar.getQuickFilterPlugin().quickFilter.focus();
1894 if ([e.BACKSPACE, e.DELETE].indexOf(e.getKey()) !== -1) {
1895 if (!this.grid.editing && !this.grid.adding && !this.action_deleteRecord.isDisabled()) {
1896 this.onDeleteRecords.call(this);
1907 onRowClick: function(grid, row, e) {
1908 /* TODO check if we need this in IE
1909 // hack to get percentage editor working
1910 var cell = Ext.get(grid.getView().getCell(row,1));
1911 var dom = cell.child('div:last');
1912 while (cell.first()) {
1913 cell = cell.first();
1914 cell.on('click', function(e){
1915 e.stopPropagation();
1916 grid.fireEvent('celldblclick', grid, row, 1, e);
1921 // fix selection of one record if shift/ctrl key is not pressed any longer
1922 if (e.button === 0 && !e.shiftKey && !e.ctrlKey && ! Ext.isTouchDevice) {
1923 var sm = grid.getSelectionModel();
1925 if (sm.getCount() == 1 && sm.isSelected(row)) {
1929 sm.clearSelections();
1930 sm.selectRow(row, false);
1931 grid.view.focusRow(row);
1936 * row doubleclick handler
1942 onRowDblClick: function(grid, row, e) {
1943 this.onEditInNewWindow.call(this, {actionType: 'edit'});
1947 * called on row context click
1949 * @param {Ext.grid.GridPanel} grid
1950 * @param {Number} row
1951 * @param {Ext.EventObject} e
1953 onRowContextMenu: function(grid, row, e) {
1955 var selModel = grid.getSelectionModel();
1956 if (!selModel.isSelected(row)) {
1957 // disable preview update if config option is set to false
1958 this.updateOnSelectionChange = this.updateDetailsPanelOnCtxMenu;
1959 selModel.selectRow(row);
1962 var contextMenu = this.getContextMenu(grid, row, e);
1965 contextMenu.showAt(e.getXY());
1968 // reset preview update
1969 this.updateOnSelectionChange = true;
1973 * Opens the required EditDialog
1974 * @param {Object} actionButton the button the action was called from
1975 * @param {Tine.Tinebase.data.Record} record the record to display/edit in the dialog
1976 * @param {Array} plugins the plugins used for the edit dialog
1977 * @param {Object} additionalConfig plain Object, which will be applied to the edit dialog on initComponent
1980 onEditInNewWindow: function(button, record, plugins, additionalConfig) {
1982 if (button.actionType == 'edit' || button.actionType == 'copy') {
1983 if (! this.action_editInNewWindow || this.action_editInNewWindow.isDisabled()) {
1984 // if edit action is disabled or not available, we also don't open a new window
1987 var selectedRows = this.grid.getSelectionModel().getSelections();
1988 record = selectedRows[0];
1990 record = this.createNewRecord();
1994 // plugins to add to edit dialog
1995 var plugins = plugins ? plugins : [];
1997 var totalcount = this.selectionModel.getCount(),
1998 selectedRecords = [],
1999 fixedFields = (button.hasOwnProperty('fixedFields') && Ext.isObject(button.fixedFields)) ? Ext.encode(button.fixedFields) : null,
2000 editDialogClass = this.editDialogClass || Tine[this.app.appName][this.recordClass.getMeta('modelName') + 'EditDialog'],
2001 additionalConfig = additionalConfig ? additionalConfig : {};
2003 // add "multiple_edit_dialog" plugin to dialog, if required
2004 if (((totalcount > 1) && (this.multipleEdit) && (button.actionType == 'edit'))) {
2005 Ext.each(this.selectionModel.getSelections(), function(record) {
2006 selectedRecords.push(record.data);
2010 ptype: 'multiple_edit_dialog',
2011 selectedRecords: selectedRecords,
2012 selectionFilter: this.selectionModel.getSelectionFilter(),
2013 isFilterSelect: this.selectionModel.isFilterSelect,
2014 totalRecordCount: totalcount
2018 Tine.log.debug('GridPanel::onEditInNewWindow');
2019 Tine.log.debug(record);
2021 var popupWindow = editDialogClass.openWindow(Ext.copyTo(
2022 this.editDialogConfig || {}, {
2023 plugins: plugins ? Ext.encode(plugins) : null,
2024 fixedFields: fixedFields,
2025 additionalConfig: Ext.encode(additionalConfig),
2026 record: editDialogClass.prototype.mode == 'local' ? Ext.encode(record.data) : record,
2027 recordId: record.getId(),
2028 copyRecord: (button.actionType == 'copy'),
2031 'update': ((this.selectionModel.getCount() > 1) && (this.multipleEdit)) ? this.onUpdateMultipleRecords : this.onUpdateRecord
2033 }, 'record,recordId,listeners,fixedFields,copyRecord,plugins,additionalConfig')
2041 * @returns {Tine.Tinebase.data.Record}
2043 createNewRecord: function() {
2044 return new this.recordClass(this.recordClass.getDefaultData(), 0);
2048 * is called after multiple records have been updated
2050 onUpdateMultipleRecords: function() {
2051 this.store.reload();
2055 * on update after edit
2057 * @param {String|Tine.Tinebase.data.Record} record
2058 * @param {String} mode
2060 onUpdateRecord: function(record, mode) {
2061 if (! this.rendered) {
2065 if (! mode && ! this.recordProxy) {
2066 // proxy-less = local if not defined otherwise
2070 if (Ext.isString(record)) {
2071 record = this.recordProxy
2072 ? this.recordProxy.recordReader({responseText: record})
2073 : Tine.Tinebase.data.Record.setFromJson(record, this.recordClass);
2075 } else if (record && Ext.isFunction(record.copy)) {
2076 record = record.copy();
2079 if (record.id === 0) {
2080 // we need to set a id != 0 to make identity handling in stores possible
2081 // TODO add config for this behaviour?
2082 record.id = 'new-' + Ext.id();
2083 record.setId(record.id);
2086 Tine.log.debug('Tine.widgets.grid.GridPanel::onUpdateRecord() -> record:');
2087 Tine.log.debug(record, mode);
2089 if (record && Ext.isFunction(record.copy)) {
2090 var idx = this.getStore().indexOfId(record.id);
2092 // only run do this in local mode as we reload the store in remote mode
2093 // NOTE: this would otherwise delete the record if a record proxy exists!
2094 if (mode == 'local') {
2095 var isSelected = this.getGrid().getSelectionModel().isSelected(idx);
2096 this.getStore().removeAt(idx);
2097 this.getStore().insert(idx, [record]);
2099 this.getGrid().getSelectionModel().selectRow(idx, true);
2103 this.getStore().add([record]);
2105 this.addToEditBuffer(record);
2108 if (mode == 'local') {
2109 this.onStoreUpdate(this.getStore(), record, Ext.data.Record.EDIT);
2112 removeStrategy: 'keepBuffered'
2117 onMoveRecords: function() {
2118 var containerSelectDialog = new Tine.widgets.container.SelectionDialog({
2119 recordClass: this.recordClass
2121 containerSelectDialog.on('select', function(dlg, node) {
2122 var sm = this.grid.getSelectionModel(),
2123 records = sm.getSelections(),
2124 recordIds = [].concat(records).map(function(v){ return v.id; }),
2125 filter = sm.getSelectionFilter(),
2126 containerId = node.attributes.id,
2127 i18nItems = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), records.length);
2129 if (! this.moveMask) {
2130 this.moveMask = new Ext.LoadMask(this.grid.getEl(), {
2131 msg: String.format(i18n._('Moving {0}'), i18nItems)
2134 this.moveMask.show();
2136 // move records to folder
2139 method: 'Tinebase_Container.moveRecordsToContainer',
2140 targetContainerId: containerId,
2142 model: this.recordClass.getMeta('modelName'),
2143 applicationName: this.recordClass.getMeta('appName')
2146 success: function() {
2147 this.refreshAfterEdit(recordIds);
2148 this.onAfterEdit(recordIds);
2150 failure: function (exception) {
2151 this.refreshAfterEdit(recordIds);
2152 this.loadGridData();
2153 Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
2160 * is called to resolve conflicts from 2 records
2162 onResolveDuplicates: function() {
2163 // TODO: allow more than 2 records
2164 if (this.grid.getSelectionModel().getSelections().length != 2) return;
2166 var selections = [];
2167 Ext.each(this.grid.getSelectionModel().getSelections(), function(sel) {
2168 selections.push(sel.data);
2171 var window = Tine.widgets.dialog.DuplicateMergeDialog.getWindow({
2172 selections: Ext.encode(selections),
2173 appName: this.app.name,
2174 modelName: this.recordClass.getMeta('modelName')
2177 window.on('contentschange', function() { this.store.reload(); }, this);
2181 * add record to edit buffer
2183 * @param {String|Tine.Tinebase.data.Record} record
2185 addToEditBuffer: function(record) {
2187 var recordData = (Ext.isString(record)) ? Ext.decode(record) : record.data,
2188 id = recordData[this.recordClass.getMeta('idProperty')];
2190 if (this.editBuffer.indexOf(id) === -1) {
2191 this.editBuffer.push(id);
2196 * generic delete handler
2198 onDeleteRecords: function(btn, e) {
2199 var sm = this.grid.getSelectionModel();
2201 if (sm.isFilterSelect && ! this.filterSelectionDelete) {
2202 Ext.MessageBox.show({
2203 title: i18n._('Not Allowed'),
2204 msg: i18n._('You are not allowed to delete all pages at once'),
2205 buttons: Ext.Msg.OK,
2206 icon: Ext.MessageBox.INFO
2211 var records = sm.getSelections();
2213 if (this.disableDeleteConfirmation || (Tine[this.app.appName].registry.get('preferences')
2214 && Tine[this.app.appName].registry.get('preferences').get('confirmDelete') !== null
2215 && Tine[this.app.appName].registry.get('preferences').get('confirmDelete') == 0)
2217 // don't show confirmation question for record deletion
2218 this.deleteRecords(sm, records);
2220 var recordNames = records[0].getTitle();
2221 if (records.length > 1) {
2222 recordNames += ', ...';
2225 var i18nQuestion = this.i18nDeleteQuestion ?
2226 this.app.i18n.n_hidden(this.i18nDeleteQuestion[0], this.i18nDeleteQuestion[1], records.length) :
2227 String.format(i18n.ngettext('Do you really want to delete the selected record ({0})?',
2228 'Do you really want to delete the selected records ({0})?', records.length), recordNames);
2229 Ext.MessageBox.confirm(i18n._('Confirm'), i18nQuestion, function(btn) {
2231 this.deleteRecords(sm, records);
2240 * @param {SelectionModel} sm
2241 * @param {Array} records
2243 deleteRecords: function(sm, records) {
2244 // directly remove records from the store (only for non-filter-selection)
2245 if (Ext.isArray(records) && ! (sm.isFilterSelect && this.filterSelectionDelete)) {
2246 Ext.each(records, function(record) {
2247 this.store.remove(record);
2249 // if nested in an editDialog, just change the parent record
2250 if (this.editDialog) {
2252 this.store.each(function(item) {
2253 items.push(item.data);
2255 this.editDialog.record.set(this.editDialogRecordProperty, items);
2256 this.editDialog.fireEvent('updateDependent');
2261 if (this.recordProxy) {
2262 if (this.usePagingToolbar) {
2263 this.pagingToolbar.refresh.disable();
2266 var i18nItems = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), records.length),
2267 recordIds = [].concat(records).map(function(v){ return v.id; });
2269 if (sm.isFilterSelect && this.filterSelectionDelete) {
2270 if (! this.deleteMask) {
2271 this.deleteMask = new Ext.LoadMask(this.grid.getEl(), {
2272 msg: String.format(i18n._('Deleting {0}'), i18nItems) + ' ' + i18n._('... This may take a long time!')
2275 this.deleteMask.show();
2278 this.deleteQueue = this.deleteQueue.concat(recordIds);
2282 success: function() {
2283 this.refreshAfterDelete(recordIds);
2284 this.onAfterDelete(recordIds);
2286 failure: function (exception) {
2287 this.refreshAfterDelete(recordIds);
2288 this.loadGridData();
2289 Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
2293 if (sm.isFilterSelect && this.filterSelectionDelete) {
2294 this.recordProxy.deleteRecordsByFilter(sm.getSelectionFilter(), options);
2296 this.recordProxy.deleteRecords(records, options);
2302 * refresh after delete (hide delete mask or refresh paging toolbar)
2304 refreshAfterDelete: function(ids) {
2305 this.deleteQueue = this.deleteQueue.diff(ids);
2307 if (this.deleteMask) {
2308 this.deleteMask.hide();
2311 if (this.usePagingToolbar) {
2312 this.pagingToolbar.refresh.show();
2317 * do something after deletion of records
2318 * - reload the store
2320 * @param {Array} [ids]
2322 onAfterDelete: function(ids) {
2323 this.editBuffer = this.editBuffer.diff(ids);
2326 removeStrategy: 'keepBuffered'
2331 * refresh after edit/move
2333 refreshAfterEdit: function(ids) {
2334 this.editBuffer = this.editBuffer.diff(ids);
2336 if (this.moveMask) {
2337 this.moveMask.hide();
2339 if (this.editMask) {
2340 this.editMask.hide();
2343 if (this.usePagingToolbar) {
2344 this.pagingToolbar.refresh.show();
2349 * do something after edit of records
2351 * @param {Array} [ids]
2353 onAfterEdit: function(ids) {
2354 this.editBuffer = this.editBuffer.diff(ids);
2357 removeStrategy: 'keepBuffered'