Merge branch '2016.11-develop-setup' into 2016.11-develop
authorPhilipp Schüle <p.schuele@metaways.de>
Fri, 23 Jun 2017 09:19:04 +0000 (11:19 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Fri, 23 Jun 2017 09:19:04 +0000 (11:19 +0200)
31 files changed:
tests/tine20/Calendar/Frontend/CalDAVTest.php
tests/tine20/Tinebase/Frontend/WebDAV/RecordTest.php
tests/tine20/Tinebase/PreferenceTest.php
tine20/Calendar/Controller/Event.php
tine20/Calendar/Frontend/Json.php
tine20/Calendar/Preference.php
tine20/Filemanager/js/FilePickerDialog.js
tine20/Filemanager/js/Filemanager.js
tine20/Filemanager/js/Model.js
tine20/Filemanager/js/NodeGridPanel.js
tine20/Filemanager/js/NodeTreePanel.js
tine20/Filemanager/js/NotificationPanel.js
tine20/Filemanager/js/SearchCombo.js
tine20/Filemanager/js/nodeActions.js
tine20/Sales/Controller/NumberableAbstract.php
tine20/Tinebase/Frontend/WebDAV/File.php
tine20/Tinebase/Model/Preference.php
tine20/Tinebase/Model/Tree/Node/Path.php
tine20/Tinebase/Preference/Abstract.php
tine20/Tinebase/Tinebase.jsb2
tine20/Tinebase/WebDav/Collection/AbstractContainerTree.php
tine20/Tinebase/css/Tinebase.css
tine20/Tinebase/js/Models.js
tine20/Tinebase/js/ux/display/DisplayTextArea.js
tine20/Tinebase/js/ux/util/urlCoder.js [new file with mode: 0644]
tine20/Tinebase/js/widgets/container/FilterModel.js
tine20/Tinebase/js/widgets/customfields/EditDialogPlugin.js
tine20/Tinebase/js/widgets/dialog/PreferencesPanel.js
tine20/Tinebase/js/widgets/grid/DetailsPanel.js
tine20/Tinebase/js/widgets/grid/PickerGridLayerCombo.js
tine20/Tinebase/js/widgets/relation/GenericPickerGridPanel.js

index 09c288e..56c2bbe 100644 (file)
@@ -38,9 +38,32 @@ class Calendar_Frontend_CalDAVTest extends TestCase
         $this->assertTrue(array_reduce($children, function($result, $container){
             return $result || $container instanceof Tasks_Frontend_WebDAV_Container;
         }, FALSE), 'tasks container is missing');
-        
+
         return $children;
     }
+
+    public function testGetUserDirectory()
+    {
+        $grants = Tinebase_Model_Grants::getPersonalGrants($this->_personas['sclever']->getId());
+        $grants->merge(new Tinebase_Record_RecordSet('Tinebase_Model_Grants', array(array(
+            'account_id' => Tinebase_Core::getUser()->getId(),
+            'account_type' => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER,
+            Tinebase_Model_Grants::GRANT_READ => true,
+            Tinebase_Model_Grants::GRANT_EXPORT => true,
+            Tinebase_Model_Grants::GRANT_SYNC => true,
+        ))));
+        $scleverTestCal = $this->_getCalendarTestContainer(Tinebase_Model_Container::TYPE_PERSONAL, $grants);
+
+        $_SERVER['HTTP_USER_AGENT'] = 'xxx';
+        $collection = new Calendar_Frontend_WebDAV(\Sabre\CalDAV\Plugin::CALENDAR_ROOT . '/' . $this->_personas['sclever']->contact_id, true);
+        $children = $collection->getChildren();
+
+        $containerIds = [];
+        foreach ($children as $child) {
+            $containerIds[] = $child->getName();
+        }
+        self::assertTrue(in_array($scleverTestCal->getId(), $containerIds));
+    }
     
     /**
      * test getChild
@@ -366,7 +389,7 @@ END:VCALENDAR&#13;
      *
      * @return Tinebase_Model_Container
      */
-    protected function _getCalendarTestContainer($type = Tinebase_Model_Container::TYPE_PERSONAL)
+    protected function _getCalendarTestContainer($type = Tinebase_Model_Container::TYPE_PERSONAL, $grants = null)
     {
         $container = Tinebase_Container::getInstance()->addContainer(new Tinebase_Model_Container(array(
             'name'              => Tinebase_Record_Abstract::generateUID(),
@@ -374,7 +397,7 @@ END:VCALENDAR&#13;
             'type'              => $type,
             'backend'           => 'Sql',
             'application_id'    => Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId(),
-        )));
+        )), $grants);
         
         return $container;
     }
index a565532..8451de3 100644 (file)
@@ -45,7 +45,10 @@ class Tinebase_Frontend_WebDAV_RecordTest extends TestCase
         $node = $this->_getWebDAVTree()->getNodeForPath('/webdav/Calendar/records/Calendar_Model_Event/' . $savedEvent->getId() . '/' . $tempFile->name);
         $this->assertEquals('text/plain', $node->getContentType());
         $this->assertEquals(17, $node->getSize());
-        
+        $handle = $node->get();
+        self::assertTrue(is_resource($handle));
+        fclose($handle);
+
         return $savedEvent;
     }
     
@@ -59,7 +62,7 @@ class Tinebase_Frontend_WebDAV_RecordTest extends TestCase
         $this->assertEquals('text/plain', $node->getContentType());
         $this->assertEquals(17, $node->getSize());
     }
-    
+
     /**
      * 
      * @return \Sabre\DAV\ObjectTree
index d32c6eb..d1a3e08 100644 (file)
@@ -17,7 +17,7 @@ require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestHelper.php'
 /**
  * Test class for Tinebase_PreferenceTest
  */
-class Tinebase_PreferenceTest extends PHPUnit_Framework_TestCase
+class Tinebase_PreferenceTest extends TestCase
 {
     /**
      * unit under test (UIT)
@@ -290,6 +290,50 @@ class Tinebase_PreferenceTest extends PHPUnit_Framework_TestCase
         }
     }
 
+    /**
+     * @see 0013214: allow to set fixed calendars as user preference
+     */
+    public function testSetFixedCalendarsPreference()
+    {
+        $personalContainer = $this->_getPersonalContainer('Calendar');
+        $data =
+          array (
+            'Calendar' =>
+            array (
+              'fixedCalendars' =>
+              array (
+                'value' =>
+                array (
+                  0 =>
+                      $personalContainer->toArray()
+                ),
+              ),
+            ),
+        );
+
+        $frontend = new Tinebase_Frontend_Json();
+        $result = $frontend->savePreferences($data, /* adminMode */ false);
+        self::assertEquals('success', $result['status']);
+
+        $preferences = $frontend->searchPreferencesForApplication('Calendar', array());
+        $fixedCalendarsPref = false;
+        foreach ($preferences['results'] as $preference) {
+            if ($preference['name'] == 'fixedCalendars') {
+                $fixedCalendarsPref = $preference;
+            }
+        }
+        self::assertTrue(is_array($fixedCalendarsPref), print_r($preferences['results'], true));
+        self::assertEquals(1, count($fixedCalendarsPref['value']), 'did not find personal container in value: '
+            . print_r($fixedCalendarsPref, true));
+        $container = $fixedCalendarsPref['value'][0];
+        self::assertTrue(is_array($container));
+        self::assertEquals($personalContainer->getId(), $container['id']);
+        self::assertTrue(isset($fixedCalendarsPref['uiconfig']), 'uiconfig missing: '
+            . print_r($fixedCalendarsPref, true));
+        $fixedCalendarIds = Calendar_Controller_Event::getInstance()->getFixedCalendarIds();
+        self::assertTrue(in_array($personalContainer->getId(), $fixedCalendarIds));
+    }
+
     /******************** protected helper funcs ************************/
     
     /**
index 6ad49c1..dd2fd7b 100644 (file)
@@ -2966,4 +2966,27 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             $eventNotificationController->doSendNotifications($event, null, 'tentative');
         }
     }
+
+    /**
+     * returns active fixed calendars for users (combines config and preference)
+     *
+     * @return array
+     * @throws Tinebase_Exception_NotFound
+     */
+    public function getFixedCalendarIds()
+    {
+        $fixedCalendars = (array) Calendar_Config::getInstance()->get(Calendar_Config::FIXED_CALENDARS);
+
+        // add fixed calendars from user preference
+        $fixedCalendarsPref = Tinebase_Core::getPreference('Calendar')->getValue(Calendar_Preference::FIXED_CALENDARS);
+        if (is_array($fixedCalendarsPref)) {
+            foreach ($fixedCalendarsPref as $container) {
+                if (isset($container['id'])) {
+                    $fixedCalendars[] = $container['id'];
+                }
+            }
+        }
+
+        return $fixedCalendars;
+    }
 }
index 6c4c9e0..c67d7f7 100644 (file)
@@ -319,8 +319,8 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
         $clientFilter = $filter = $this->_decodeFilter($filter, 'Calendar_Model_EventFilter');
 
         // find out if fixed calendars should be used
-        $fixedCalendars = Calendar_Config::getInstance()->get(Calendar_Config::FIXED_CALENDARS);
-        $useFixedCalendars = is_array($fixedCalendars) && ! empty($fixedCalendars);
+        $fixedCalendarIds = Calendar_Controller_Event::getInstance()->getFixedCalendarIds();
+        $useFixedCalendars = is_array($fixedCalendarIds) && ! empty($fixedCalendarIds);
         
         $periodFilter = $filter->getFilter('period');
         
@@ -336,7 +336,7 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
         // add fixed calendar on demand
         if ($useFixedCalendars) {
             $fixed = new Calendar_Model_EventFilter(array(), 'AND');
-            $fixed->addFilter( new Tinebase_Model_Filter_Text('container_id', 'in', $fixedCalendars));
+            $fixed->addFilter( new Tinebase_Model_Filter_Text('container_id', 'in', $fixedCalendarIds));
             
             $fixed->addFilter($periodFilter);
             
@@ -358,7 +358,7 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
             'filter'        => $clientFilter->toArray(TRUE),
         );
     }
-    
+
     /**
      * get default period filter
      * 
index 9d226a4..d88de28 100644 (file)
@@ -98,6 +98,11 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
     const DEFAULT_CALENDAR_STRATEGY = 'defaultCalendarStrategy';
 
     /**
+     * fixedCalendars
+     */
+    const FIXED_CALENDARS = 'fixedCalendars';
+
+    /**
      * @var string application
      */
     protected $_application = 'Calendar';
@@ -127,7 +132,8 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
             self::DEFAULTATTENDEE_STRATEGY,
             self::DEFAULT_EVENTS_RRIVATE,
             self::FIRSTDAYOFWEEK,
-            self::DEFAULT_CALENDAR_STRATEGY
+            self::DEFAULT_CALENDAR_STRATEGY,
+            self::FIXED_CALENDARS,
         );
         
         if ($cropDays) {
@@ -211,6 +217,10 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
                 'label'         => $translate->_('Default calendar strategy'),
                 'description'   => $translate->_('The calendar for new events if no container is selected'),
             ),
+            self::FIXED_CALENDARS => array(
+                'label'         => $translate->_('Fixed Calendars'),
+                'description'   => $translate->_('Calendars always selected regardless of all filter parameters.'),
+            ),
         );
         
         return $prefDescriptions;
@@ -436,10 +446,44 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
                         </option>
                     </options>';
                 break;
+            case self::FIXED_CALENDARS:
+                $preference->value = array();
+                // TODO set better (?) options / maybe this could be removed
+                $preference->options    = '<?xml version="1.0" encoding="UTF-8"?>
+                    <options>
+                        <special>' . Tinebase_Preference_Abstract::DEFAULTCONTAINER_OPTIONS . '</special>
+                    </options>';
+
+                $preference->uiconfig = array(
+                    'xtype'       => 'containerspicker',
+                    'appName'     => 'Calendar',
+                    'model'       => 'Event',
+                );
+                $preference->personal_only = true;
+                break;
             default:
                 throw new Tinebase_Exception_NotFound('Default preference with name ' . $_preferenceName . ' not found.');
         }
         
         return $preference;
     }
+
+    /**
+     * overwrite this in concrete classes if needed
+     *
+     * @param $_preferenceName
+     * @return array
+     */
+    public function _getPrefRecordConfig($_preferenceName)
+    {
+        switch ($_preferenceName) {
+            case self::FIXED_CALENDARS:
+                return array(
+                    'modelName'      => 'Tinebase_Model_Container',
+                );
+                break;
+            default:
+                return parent::_getPrefRecordConfig($_preferenceName);
+        }
+    }
 }
index 9907686..b899138 100644 (file)
@@ -68,6 +68,8 @@ Tine.Filemanager.FilePickerDialog = Ext.extend(Ext.Panel, {
      */
     constraint: null,
 
+    cls: 'tw-editdialog',
+
     /**
      * Constructor.
      */
@@ -97,7 +99,7 @@ Tine.Filemanager.FilePickerDialog = Ext.extend(Ext.Panel, {
             scope: this
         });
 
-        this.bbar = [
+        this.fbar = [
             '->',
             this.okAction
         ];
index a32db04..4659e8f 100644 (file)
@@ -35,6 +35,8 @@ Tine.Filemanager.Application = Ext.extend(Tine.Tinebase.Application, {
     showNode: function(path) {
         this.getMainScreen().getCenterPanel().initialLoadAfterRender = false;
         Tine.Tinebase.MainScreenPanel.show(this);
+        // NOTE: decodeURIComponent can't cope with +
+        path = Ext.ux.util.urlCoder.decodeURIComponent(path);
 
         // if file, show directory file is in
         var dirPath = path;
index 9143565..582cada 100644 (file)
@@ -50,6 +50,15 @@ Tine.Filemanager.Model.Node = Tine.Tinebase.data.Record.create(Tine.Tinebase.Mod
         var _ = window.lodash;
 
         return _.indexOf(['/', Tine.Tinebase.container.getMyFileNodePath(), '/personal', '/shared'], this.get('path')) >= 0;
+    },
+
+    getSystemLink: function() {
+        var _ = window.lodash,
+            encodedPath = _.map(String(this.get('path')).replace(/(^\/|\/$)/, '').split('/'), Ext.ux.util.urlCoder.encodeURIComponent).join('/');
+
+        return [Tine.Tinebase.common.getUrl().replace(/\/$/, ''), '#/Filemanager/showNode', encodedPath].join('/');
+
+
     }
 });
 
index aaca82d..141cab5 100644 (file)
@@ -458,6 +458,7 @@ Tine.Filemanager.NodeGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
         this.action_download = Tine.Filemanager.nodeActionsMgr.get('download');
         this.action_moveRecord = Tine.Filemanager.nodeActionsMgr.get('move');
         this.action_publish = Tine.Filemanager.nodeActionsMgr.get('publish');
+        this.action_systemLink = Tine.Filemanager.nodeActionsMgr.get('systemLink');
 
         if (this.previewsEnabled) {
             this.action_preview = Tine.Filemanager.nodeActionsMgr.get('preview');
@@ -482,7 +483,7 @@ Tine.Filemanager.NodeGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
             }.createDelegate(this)
         });
 
-        var contextActions = [this.action_deleteRecord, 'rename', this.action_moveRecord, this.action_download, 'resume', 'pause', this.action_editFile, this.action_publish];
+        var contextActions = [this.action_deleteRecord, 'rename', this.action_moveRecord, this.action_download, 'resume', 'pause', this.action_editFile, this.action_publish, this.action_systemLink];
 
         if (this.previewsEnabled) {
             contextActions.push(this.action_preview);
@@ -498,7 +499,7 @@ Tine.Filemanager.NodeGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
 
         this.folderContextMenu = Tine.Filemanager.nodeContextMenu.getMenu({
             nodeName: Tine.Filemanager.Model.Node.getContainerName(),
-            actions: [this.action_deleteRecord, 'rename', this.action_moveRecord, this.action_editFile, this.action_publish],
+            actions: [this.action_deleteRecord, 'rename', this.action_moveRecord, this.action_editFile, this.action_publish, this.action_systemLink],
             scope: this,
             backend: 'Filemanager',
             backendModel: 'Node'
@@ -514,7 +515,8 @@ Tine.Filemanager.NodeGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
             this.action_download,
             this.action_deleteRecord,
             this.action_editFile,
-            this.action_publish
+            this.action_publish,
+            this.action_systemLink
         ];
 
         if (this.previewsEnabled) {
@@ -594,6 +596,11 @@ Tine.Filemanager.NodeGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
                     scale: 'medium',
                     rowspan: 2,
                     iconAlign: 'top'
+                }),
+                Ext.apply(new Ext.Button(this.action_systemLink), {
+                    scale: 'medium',
+                    rowspan: 2,
+                    iconAlign: 'top'
                 })
             ];
 
index 3b1ca88..fbc21f6 100644 (file)
@@ -298,7 +298,7 @@ Tine.Filemanager.NodeTreePanel = Ext.extend(Tine.widgets.container.TreePanel, {
         this.ctxMenu = Tine.Filemanager.nodeContextMenu.getMenu({
             actionMgr: Tine.Filemanager.nodeActionsMgr,
             nodeName: this.recordClass.getContainerName(),
-            actions: ['reload', 'createFolder', 'delete', 'rename', 'move', 'edit', 'publish'],
+            actions: ['reload', 'createFolder', 'delete', 'rename', 'move', 'edit', 'publish', 'systemLink'],
             scope: this,
             backend: 'Filemanager',
             backendModel: 'Node'
index 1253256..ef706b4 100644 (file)
@@ -30,7 +30,8 @@ Tine.Filemanager.NotificationPanel = Ext.extend(Ext.Panel, {
             idProperty: 'accountId'
         });
 
-        var disable = window.lodash.get(this.editDialog.record, 'data.notificationProps', []).length === 0;
+        var notificationProps = window.lodash.get(this.editDialog.record, 'data.notificationProps', []);
+        var disable = notificationProps === null || notificationProps.length === 0;
 
         this.notificationGrid = new Tine.Filemanager.NotificationGridPanel({
             store: store,
@@ -39,9 +40,19 @@ Tine.Filemanager.NotificationPanel = Ext.extend(Ext.Panel, {
             editDialog: this.editDialog
         });
 
+        var featureEnabled = _.get(Tine.Tinebase.configManager.get('filesystem'), 'enableNotifications', false);
+
+        var disabled = false;
+
+        if (!featureEnabled) {
+            disabled = true;
+        } else if (!_.get(this.editDialog, 'record.data.account_grants.adminGrant', false)) {
+            disabled = true;
+        }
+
         this.hasOwnNotificationSettings = new Ext.form.Checkbox({
             checked: !disable,
-            disabled: !_.get(this.editDialog, 'record.data.account_grants.adminGrant', false) && !disable,
+            disabled: disabled,
             boxLabel: this.app.i18n._('This folder has own notification settings'),
             listeners: {scope: this, check: this.onOwnNotificationCheck}
         });
index 7047405..e89f4af 100644 (file)
@@ -5,7 +5,8 @@
  * @package     Filemanager
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
- * @copyright   Copyright (c) 2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Michael Spahn <m.spahn@metaways.de>
+ * @copyright   Copyright (c) 2012-2017 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 
@@ -16,71 +17,65 @@ Ext.ns('Tine.Filemanager');
  * 
  * @namespace   Tine.Filemanager
  * @class       Tine.Filemanager.SearchCombo
- * @extends     Ext.form.ComboBox
+ * @extends     Ext.form.TriggerField
  * 
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
- * @copyright   Copyright (c) 2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Michael Spahn <m.spahn@metaways.de>
+ * @copyright   Copyright (c) 2012-2017 Metaways Infosystems GmbH (http://www.metaways.de)
  * 
  * @param       {Object} config
  * @constructor
  * Create a new Tine.Filemanager.SearchCombo
  */
-Tine.Filemanager.SearchCombo = Ext.extend(Tine.Tinebase.widgets.form.RecordPickerComboBox, {
+Tine.Filemanager.SearchCombo = Ext.extend(Ext.form.TriggerField, {
     
     allowBlank: false,
     itemSelector: 'div.search-item',
     minListWidth: 200,
+
+    app: null,
+    
+    recordClass: null,
+    recordProxy: null,
     
-    //private
     initComponent: function(){
         this.recordClass = Tine.Filemanager.Model.Node;
-        this.recordProxy = Tine.Filemanager.recordBackend;
-        this.additionalFilters = [
-            {field: 'recursive', operator: 'equals', value: true },
-            {field: 'path', operator: 'equals', value: '/' }
-        ];
-        this.initTemplate();
-        Tine.Filemanager.SearchCombo.superclass.initComponent.call(this);
-    },
-    
-    /**
-     * init template
-     * @private
-     */
-    initTemplate: function() {
-        // Custom rendering Template
-        // TODO move style def to css ?
-        if (! this.tpl) {
-            this.tpl = new Ext.XTemplate(
-                '<tpl for="."><div class="search-item">',
-                    '<table cellspacing="0" cellpadding="2" border="0" style="font-size: 11px;" width="100%">',
-                        '<tr>',
-                            '<td ext:qtip="{[this.renderPathName(values)]}" style="height:16px">{[this.renderFileName(values)]}</td>',
-                        '</tr>',
-                    '</table>',
-                '</div></tpl>',
-                {
-                    renderFileName: function(values) {
-                        return Ext.util.Format.htmlEncode(values.name);
-                    },
-                    renderPathName: function(values) {
-                        return Ext.util.Format.htmlEncode(values.path.replace(values.name, ''));
-                    }
-                    
-                }
-            );
+        this.recordProxy = Tine.Filemanager.fileRecordBackend;
+
+        if (null === this.app) {
+            this.app = Tine.Tinebase.appMgr.get('Filemanager');
         }
+
+        this.supr().initComponent.call(this);
+
+        this.addEvents(
+            /**
+             * @param selected node
+             */
+            'select'
+        );
     },
     
-    getValue: function() {
-            return Tine.Filemanager.SearchCombo.superclass.getValue.call(this);
-    },
+    onTriggerClick: function () {
+        var filepicker = new Tine.Filemanager.FilePickerDialog({
+            title: this.app.i18n._('Select a file'),
+            singleSelect: true,
+            constraint: 'file'
+        });
 
-    setValue: function (value) {
-        return Tine.Filemanager.SearchCombo.superclass.setValue.call(this, value);
-    }
+        filepicker.on('selected', function (node) {
+            if (!node || 0 === node.length) {
+                return true;
+            }
 
+            this.fireEvent('select', node[0]);
+            this.setValue(node[0].path);
+
+        }, this);
+
+        filepicker.openWindow();
+    }
 });
 
 Tine.widgets.form.RecordPickerManager.register('Filemanager', 'Node', Tine.Filemanager.SearchCombo);
index 65546c2..bc40890 100644 (file)
@@ -153,6 +153,37 @@ Tine.Filemanager.nodeActions.Rename = {
 };
 
 /**
+ * single file or directory node with editGrant
+ */
+Tine.Filemanager.nodeActions.SystemLink = {
+    app: 'Filemanager',
+    requiredGrant: 'readGrant',
+    allowMultiple: false,
+    text: 'System Link', // _('System Link')
+    iconCls: 'action_system_link',
+    disabled: true,
+    // actionType: 'edit',
+    scope: this,
+    handler: function () {
+        var _ = window.lodash,
+            app = this.initialConfig.app,
+            record = this.initialConfig.selections[0];
+
+        Ext.MessageBox.show({
+            title: i18n._('System Link'),
+            // minWidth:
+            maxWidth: screen.availWidth,
+            msg: '<b>' + i18n._('Use this link to share the entry with other system users') + ':</b><br>'
+                    + record.getSystemLink(),
+            buttons: Ext.MessageBox.OK,
+            // value: record.getSystemLink(),
+            // prompt: true,
+            icon: Ext.MessageBox.INFO
+        });
+    }
+};
+
+/**
  * one or multiple nodes, all need deleteGrant
  */
 Tine.Filemanager.nodeActions.Delete = {
index 7c52589..53ebd42 100644 (file)
@@ -41,7 +41,7 @@ abstract class Sales_Controller_NumberableAbstract extends Tinebase_Controller_R
      * @param integer $number
      * @return integer
      */
-    public function setNumberZerofill(integer $number = NULL)
+    public function setNumberZerofill($number = NULL)
     {
         $this->_numberZerofill = $number;
         
@@ -54,7 +54,7 @@ abstract class Sales_Controller_NumberableAbstract extends Tinebase_Controller_R
      * @param string $prefix
      * @return string
      */
-    public function setNumberPrefix(string $prefix = NULL)
+    public function setNumberPrefix($prefix = NULL)
     {
         $this->_numberPrefix = $prefix;
         
index ff6f2a0..0bf0817 100644 (file)
@@ -18,7 +18,12 @@ class Tinebase_Frontend_WebDAV_File extends Tinebase_Frontend_WebDAV_Node implem
 {
     public function get() 
     {
-        if (!Tinebase_Core::getUser()->hasGrant($this->_getContainer(), Tinebase_Model_Grants::GRANT_DOWNLOAD)) {
+        $pathRecord = Tinebase_Model_Tree_Node_Path::createFromStatPath($this->_path);
+        if (! $pathRecord->isRecordPath() && ! Tinebase_Core::getUser()->hasGrant(
+                $this->_getContainer(),
+                Tinebase_Model_Grants::GRANT_DOWNLOAD
+            )
+        ) {
             throw new Sabre\DAV\Exception\Forbidden('Forbidden to download file: ' . $this->_path);
         }
         $handle = Tinebase_FileSystem::getInstance()->fopen($this->_path, 'r');
index 384a8b3..fe53bb6 100644 (file)
@@ -91,5 +91,64 @@ class Tinebase_Model_Preference extends Tinebase_Record_Abstract
         'personal_only'      => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
     // don't allow user to change value
         'locked'      => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
+        // multiselection preference
+        //'multiselect'        =>  array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => false),
+        'uiconfig'        =>  array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
+        'recordConfig'    =>  array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
     );
+
+    /**
+     * TODO remove when converted to ModelConfig
+     * TODO generalize / support other models
+     */
+    public function runConvertToRecord()
+    {
+        // convert value property if necessary
+        if (Tinebase_Helper::is_json($this->value)) {
+            $value = Tinebase_Helper::jsonDecode($this->value);
+            switch ($value['modelName']) {
+                case 'Tinebase_Model_Container':
+                    $containers = array();
+                    foreach ($value['ids'] as $containerId) {
+                        try {
+                            $container = Tinebase_Container::getInstance()->getContainerById($containerId);
+                            // TODO should be converted to array by json frontend
+                            $containers[] = $container->toArray();
+                        } catch (Exception $e) {
+                            // not found / no access / ...
+                        }
+                    }
+                    $this->value = $containers;
+                    break;
+                default:
+                    throw new Tinebase_Exception_InvalidArgument('model not supported');
+
+            }
+        } else {
+            parent::runConvertToRecord();
+        }
+    }
+
+    /**
+     * TODO remove when converted to ModelConfig
+     * TODO generalize / support other models
+     */
+    public function runConvertToData()
+    {
+        // convert value property if necessary
+        if (is_array($this->value) && is_array($this->recordConfig) && isset($this->recordConfig['modelName'])) {
+            $value = array(
+                'modelName' => $this->recordConfig['modelName'],
+                'ids' => array(),
+            );
+            foreach ($this->value as $record) {
+                if (isset($record['id'])) {
+                    $value['ids'][] = $record['id'];
+                }
+            }
+            $this->value = json_encode($value);
+        } else {
+            parent::runConvertToData();
+        }
+    }
 }
index 670bf59..d025c0d 100644 (file)
@@ -58,6 +58,11 @@ class Tinebase_Model_Tree_Node_Path extends Tinebase_Record_Abstract
     const FOLDERS_PART = 'folders';
 
     /**
+     * records path part
+     */
+    const RECORDS_PART = 'records';
+
+    /**
      * key in $_validators/$_properties array for the field which 
      * represents the identifier
      * 
@@ -456,6 +461,18 @@ class Tinebase_Model_Tree_Node_Path extends Tinebase_Record_Abstract
     }
 
     /**
+     * returns true if path belongs to a record or record attachment
+     *
+     * @return bool
+     * @throws Tinebase_Exception_InvalidArgument
+     */
+    public function isRecordPath()
+    {
+        $parts = $this->_getPathParts();
+        return (count($parts) > 2 && $parts[2] === self::RECORDS_PART);
+    }
+
+    /**
      * validate node/container existance
      * 
      * @throws Tinebase_Exception_NotFound
index 4011fde..0b6e2b2 100644 (file)
@@ -291,7 +291,7 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
         
         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
             . ' ' . print_r($queryResult, true));
-        
+
         if (! $queryResult) {
             $pref = $this->getApplicationPreferenceDefaults($_preferenceName, $_accountId, $_accountType);
         } else {
@@ -444,7 +444,8 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
                     'value'             => $_value,
                     'account_id'        => $_accountId,
                     'account_type'      => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER,
-                    'type'              => Tinebase_Model_Preference::TYPE_USER
+                    'type'              => Tinebase_Model_Preference::TYPE_USER,
+                    'recordConfig'      => $this->_getPrefRecordConfig($_preferenceName),
                 ));
                 $this->create($preference);
                 $action = 'Created';
@@ -471,13 +472,26 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
                 $action = 'Reset';
             } else {
                 $preference->value = $_value;
+                $preference->recordConfig = $this->_getPrefRecordConfig($_preferenceName);
                 $this->update($preference);
                 $action = 'Updated';
             }
         }
         
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
-            . ' ' . $action . ': ' . $_preferenceName . ' for user ' . $_accountId . ' -> ' . $_value);
+            . ' ' . $action . ': ' . $_preferenceName . ' for user ' . $_accountId . ' -> '
+            . (is_array($_value) ? print_r($_value, true) : $_value));
+    }
+
+    /**
+     * overwrite this in concrete classes if needed
+     *
+     * @param $_preferenceName
+     * @return array
+     */
+    public function _getPrefRecordConfig($_preferenceName)
+    {
+        return array();
     }
 
     /**
@@ -525,6 +539,9 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
                 }
             }
             // add default setting to the top of options
+            if (is_array($valueLabel)) {
+                $valueLabel = implode(',', $valueLabel);
+            }
             $defaultLabel = Tinebase_Translation::getTranslation('Tinebase')->_('default') . 
                 ' (' . $valueLabel . ')';
             
@@ -532,6 +549,10 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
                 Tinebase_Model_Preference::DEFAULT_VALUE,
                 $defaultLabel,
             ));
+
+            if (isset($default->uiconfig)) {
+                $_preference->uiconfig = $default->uiconfig;
+            }
         }
         
         $_preference->options = $options;
@@ -790,7 +811,7 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
             case self::DEFAULTCONTAINER_OPTIONS:
                 $result = $this->_getDefaultContainerOptions();
                 break;
-                
+
             case self::DEFAULTPERSISTENTFILTER:
                 $result = Tinebase_PersistentFilter::getPreferenceValues($this->_application);
                 break;
@@ -827,7 +848,7 @@ abstract class Tinebase_Preference_Abstract extends Tinebase_Backend_Sql_Abstrac
         }
         return $result;
     }
-    
+
     /**
      * adds defaults to default container pref
      * 
index cfeea6c..e1db9a1 100644 (file)
           "path": "js/ux/util/"
         },
         {
+          "text": "urlCoder.js",
+          "path": "js/ux/util/"
+        },
+        {
           "text": "FieldLabeler.js",
           "path": "js/ux/"
         },
index 4ce15d0..5463274 100644 (file)
@@ -548,6 +548,7 @@ abstract class Tinebase_WebDav_Collection_AbstractContainerTree
                         )
                     );
                 } else {
+                    // NOTE: seems to be the expected behavior for non-delegation clients
                     $containers = $this->_containerController->getContainerByACL(Tinebase_Core::getUser(), $this->_getApplicationName(),  array(
                         Tinebase_Model_Grants::GRANT_READ,
                         Tinebase_Model_Grants::GRANT_SYNC
index 04f0886..f8fed64 100644 (file)
@@ -94,6 +94,16 @@ html, body {
 }
 
 
+.action_system_link {
+    background-image:url(../../images/oxygen/16x16/places/link.png) !important;
+}
+.x-btn-medium .action_system_link {
+    background-image:url(../../images/oxygen/22x22/places/link.png) !important;
+}
+.x-btn-large .action_system_link {
+    background-image:url(../../images/oxygen/32x32/places/link.png) !important;
+}
+
 .action_rename {
     background-image:url(../../images/oxygen/16x16/actions/document-properties.png) !important;
 }
index 89b2b26..f59eb1b 100644 (file)
@@ -255,6 +255,7 @@ Tine.Tinebase.Model.Preference = Ext.data.Record.create([
     {name: 'description'    },
     {name: 'personal_only',         type: 'boolean' },
     {name: 'locked',                type: 'boolean' },
+    {name: 'uiconfig'       },
     {name: 'options'        }
 ]);
 
index d4f42d8..9192125 100644 (file)
@@ -20,6 +20,8 @@ Ext.ux.display.DisplayTextArea = Ext.extend(Ext.Container, {
     nl2br: true,
     linkify: true,
 
+    autoScroll: true,
+
     layout: 'fit',
     cls: 'x-ux-display-textarea',
 
diff --git a/tine20/Tinebase/js/ux/util/urlCoder.js b/tine20/Tinebase/js/ux/util/urlCoder.js
new file mode 100644 (file)
index 0000000..eda88a7
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Tine 2.0
+ *
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Cornelius Weiss <c.weiss@metaways.de>
+ * @copyright   Copyright (c) 2017 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+Ext.ns('Ext.ux.util.urlCoder');
+
+/**
+ * native encodeURIComponent converts space into %20 -> convert to modern +
+ *
+ * @param string
+ * @returns {string}
+ */
+Ext.ux.util.urlCoder.encodeURIComponent = function(string) {
+    return encodeURIComponent(string).replace(/\%20/gm,"+");
+};
+
+/**
+ * native encodeURI converts space into %20 -> convert to modern +
+ *
+ * @param string
+ * @returns {string}
+ */
+Ext.ux.util.urlCoder.encodeURI = function(string) {
+    return encodeURI(string).replace(/\%20/gm,"+");
+};
+
+/**
+ * native decodeURIComponent converts can't cope with modern + for spaces
+ *
+ * @param string
+ * @returns {string}
+ */
+Ext.ux.util.urlCoder.decodeURIComponent = function(string) {
+    return decodeURIComponent((string+'').replace(/\+/gm,"%20"));
+};
+
+/**
+ * native decodeURI converts can't cope with modern + for spaces
+ *
+ * @param string
+ * @returns {string}
+ */
+Ext.ux.util.urlCoder.decodeURI = function(string) {
+    return decodeURI((string+'').replace(/\+/gm,"%20"));
+};
\ No newline at end of file
index 9cb8753..0fa7234 100644 (file)
@@ -391,3 +391,4 @@ Tine.widgets.container.FilterModelMultipleValueField = Ext.extend(Ext.ux.form.La
     }
 });
 
+Ext.reg('containerspicker', Tine.widgets.container.FilterModelMultipleValueField);
\ No newline at end of file
index 74f8737..2c89943 100644 (file)
@@ -68,12 +68,8 @@ Tine.widgets.customfields.EditDialogPlugin.prototype = {
                 
                 if (field) {
                     if(field.isXType('combo') && Ext.isObject(this.customfieldsValue[name])) {
-                        var phpClassName = cfConfig.get('model').split('_Model_'),
-                            recordClass = Tine[phpClassName[0]].Model[phpClassName[1]],
-                            record = new recordClass(this.customfieldsValue[name]);
-                            
-                        field.setValue(record.getId());
-                        field.selectedRecord = record.data; 
+                        var record = new field.recordClass(this.customfieldsValue[name]);
+                        field.setValue(record);
                     } else {
                         field.setValue(this.customfieldsValue[name]);
                     }
index 9307dcf..14b834b 100644 (file)
@@ -112,14 +112,23 @@ Tine.widgets.dialog.PreferencesPanel = Ext.extend(Ext.Panel, {
                 var options = pref.get('options');
                 // NOTE: some prefs have no default and only one option (e.g. std email account)
                 if (options.length > 1 || (options.length == 1 && options[0][0] !== '_default_')) {
-                    Ext.apply(fieldDef, {
-                        xtype: (this.adminMode ? 'lockCombo' : 'combo'),
-                        store: pref.get('options'),
-                        mode: 'local',
-                        forceSelection: true,
-                        allowBlank: false,
-                        triggerAction: 'all'
-                    });
+                    if (pref.get('uiconfig') && pref.get('uiconfig').xtype) {
+                        // TODO support admin mode / currently this is personal_only
+                        Ext.apply(fieldDef, {
+                            xtype: pref.get('uiconfig').xtype,
+                            recordClass: Tine[pref.get('uiconfig').appName].Model[pref.get('uiconfig').model],
+                            value: pref.get('value') === '_default_' ? [] : pref.get('value'),
+                        });
+                    } else {
+                        Ext.apply(fieldDef, {
+                            xtype: (this.adminMode ? 'lockCombo' : 'combo'),
+                            store: pref.get('options'),
+                            mode: 'local',
+                            forceSelection: true,
+                            allowBlank: false,
+                            triggerAction: 'all'
+                        });
+                    }
                 } else {
                     Ext.apply(fieldDef, {
                         xtype: (this.adminMode ? 'lockTextfield' : 'textfield'),
index 69a0bd0..4a313c5 100644 (file)
@@ -291,4 +291,4 @@ Tine.widgets.grid.DetailsPanel = Ext.extend(Ext.Panel, {
     }
 });
 
-Ext.reg('widget-detailspanel', Tine.widgets.grid.DetailsPanel)
\ No newline at end of file
+Ext.reg('widget-detailspanel', Tine.widgets.grid.DetailsPanel);
\ No newline at end of file
index 4275775..f81eb30 100644 (file)
@@ -36,6 +36,9 @@ Tine.widgets.grid.PickerGridLayerCombo = Ext.extend(Ext.ux.form.LayerCombo, {
 
     initComponent: function () {
         Tine.widgets.grid.PickerGridLayerCombo.superclass.initComponent.call(this);
+        this.store = new Ext.data.SimpleStore({
+            fields: this.gridRecordClass
+        });
 
         this.on('beforecollapse', this.onBeforeCollapse, this);
     },
@@ -44,7 +47,8 @@ Tine.widgets.grid.PickerGridLayerCombo = Ext.extend(Ext.ux.form.LayerCombo, {
         this.pickerGrid = new Tine.widgets.grid.PickerGridPanel({
             recordClass: this.gridRecordClass,
             height: this.layerHeight - 40 || 'auto',
-            onStoreChange: Ext.emptyFn
+            onStoreChange: Ext.emptyFn,
+            store: this.store
         });
 
         return [this.pickerGrid];
@@ -64,12 +68,29 @@ Tine.widgets.grid.PickerGridLayerCombo = Ext.extend(Ext.ux.form.LayerCombo, {
      * @return {Ext.form.Field} this
      */
     setValue: function (value) {
+        var _ = window.lodash;
+
+        this.setStoreFromArray(value);
+        if (this.rendered) {
+            var titles = _.reduce(this.store.data.items, function(result, record) {
+                return result.concat(record.getTitle());
+            }, []);
+            this.setRawValue(titles.join(', '));
+        }
         this.currentValue = value;
+
         // to string overwrite, to make sure record is changed.
         Tine.Tinebase.common.assertComparable(this.currentValue);
         return this;
     },
 
+    afterRender: function () {
+
+        Tine.widgets.grid.PickerGridLayerCombo.superclass.afterRender.apply(this, arguments);
+        if (this.currentValue) {
+            this.setValue(this.currentValue);
+        }
+    },
     /**
      * sets values to innerForm (grid)
      */
@@ -95,24 +116,16 @@ Tine.widgets.grid.PickerGridLayerCombo = Ext.extend(Ext.ux.form.LayerCombo, {
      *
      * @param {Array}
      *
-     * TODO move to picker grid?
      */
     setStoreFromArray: function(data) {
         //this.pickerGrid.getStore().clearData();
-        var rawData = [];
-        this.pickerGrid.getStore().removeAll();
+        this.store.removeAll();
 
         for (var i = data.length-1; i >=0; --i) {
             var recordData = data[i],
                 newRecord = new this.gridRecordClass(recordData);
-            this.pickerGrid.getStore().insert(0, newRecord);
-            rawData.push(newRecord.getTitle());
+            this.store.insert(0, newRecord);
         }
-
-        if (rawData) {
-            this.setRawValue(rawData.join(', '));
-        }
-
     },
 
     /**
@@ -120,11 +133,10 @@ Tine.widgets.grid.PickerGridLayerCombo = Ext.extend(Ext.ux.form.LayerCombo, {
      *
      * @return {Array}
      *
-     * TODO move to picker grid?
      */
     getFromStoreAsArray: function() {
         var result = [];
-        this.pickerGrid.getStore().each(function(record) {
+        this.store.each(function(record) {
             result.push(record.data);
         }, this);
 
index adc6a60..e806de0 100644 (file)
@@ -694,9 +694,15 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
     /**
      * is called when selecting a record in the searchCombo (relationpickercombo)
      */
-    onAddRecordFromCombo: function() {
-        var record = this.getActiveSearchCombo().store.getById(this.getActiveSearchCombo().getValue());
-        
+    onAddRecordFromCombo: function(node) {
+        var record = null;
+
+        if (this.getActiveSearchCombo().hasOwnProperty('store')) {
+            record = this.getActiveSearchCombo().store.getById(this.getActiveSearchCombo().getValue())
+        } else {
+            record = node;
+        }
+
         if (! record) {
             return;
         }
@@ -708,14 +714,18 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
         }
         
         this.onAddRecord(record, relconf);
-        
-        this.getActiveSearchCombo().collapse();
-        this.getActiveSearchCombo().reset();
+
+        if (this.getActiveSearchCombo().hasOwnProperty('collapse')) {
+            this.getActiveSearchCombo().collapse();
+            this.getActiveSearchCombo().reset();
+        }
     },
 
     /**
      * call to add relation from an external component
-     * 
+     *
+     *  @todo refactor this trash
+     *
      * @param {Tine.Tinebase.data.Record} record
      * @param {Object} relconf
      */
@@ -724,16 +734,16 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
             if (! relconf) {
                 relconf = {};
             }
-            if (record.data.hasOwnProperty('relations')) {
+            if (record.data && record.data.hasOwnProperty('relations')) {
                 record.data.relations = null;
                 delete record.data.relations;
             }
             var rc = this.getActiveSearchCombo().recordClass;
             var relatedPhpModel = rc.getPhpClassName();
-            
+
             var app = rc.getMeta('appName'), model = rc.getMeta('modelName'), f = app + model;
             var type = '';
-            
+
             if (this.constraintsConfig[f] && this.constraintsConfig[f].length) {
                 // per default the first defined type is used
                 var type = this.constraintsConfig[f][0].type;
@@ -747,7 +757,7 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
                 type = '';
 
             var relationRecord = new Tine.Tinebase.Model.Relation(Ext.apply(this.getRelationDefaults(), Ext.apply({
-                related_record: record.data,
+                related_record: record.data || record,
                 related_id: record.id,
                 related_model: relatedPhpModel,
                 type: type,
@@ -772,10 +782,6 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
                 this.onAddNewRelationToStore(relationRecord, record);
             }
         }
-        
-        // reset search combo
-        this.getActiveSearchCombo().collapse();
-        this.getActiveSearchCombo().reset();
     },
     
     /**
@@ -793,7 +799,7 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
         var relatedApp = Tine.Tinebase.appMgr.get(appName); 
         var relatedConstrainsConfig = relatedApp.getRegistry().get('relatableModels');
         var ownRecordClassName = this.editDialog.recordClass.getMeta('modelName');
-        var relatedRecordProxy = Tine[appName][(model.toLowerCase() + 'Backend')];
+        var relatedRecordProxy = this.getActiveSearchCombo().recordProxy || Tine[appName][(model.toLowerCase() + 'Backend')];
         
         if (! Ext.isFunction(record.get)) {
             record = relatedRecordProxy.recordReader({responseText: Ext.encode(record)});
@@ -894,9 +900,13 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
      * @param {Tine.Tinebase.data.Record} record
      */
     onAddNewRelationToStore: function(relationRecord, record) {
-        relationRecord.data.related_record.relations = null;
-        delete relationRecord.data.related_record.relations;
-        
+        var _ = window.lodash;
+
+        if (_.get(relationRecord, 'data.related_record.relations', false)) {
+            relationRecord.data.related_record.relations = null;
+            delete relationRecord.data.related_record.relations;
+        }
+
         if (this.relationCheck(relationRecord)) {
             Tine.log.debug('Adding new relation:');
             Tine.log.debug(relationRecord);