From d92310ac2b924ad695630308f75ba11e2831e4c0 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Wed, 27 Sep 2017 12:26:24 +0200 Subject: [PATCH 001/105] Adjust settings navigation heading style Signed-off-by: Jan-Christoph Borchardt --- settings/css/settings.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/settings/css/settings.scss b/settings/css/settings.scss index ab8d69b3a4..84939b0179 100644 --- a/settings/css/settings.scss +++ b/settings/css/settings.scss @@ -1267,7 +1267,14 @@ doesnotexist:-o-prefocus, .strengthify-wrapper { .settings-caption { font-weight: bold; line-height: 44px; - padding: 0 12px; + padding: 0 44px; white-space: nowrap; text-overflow: ellipsis; + // !important to overwrite specific hover and focus + opacity: .4 !important; + box-shadow: none !important; + + &:not(:first-child) { + margin-top: 22px; + } } From 830310ad1d8b8fa52e879754cd9488530a16b47a Mon Sep 17 00:00:00 2001 From: rakekniven Date: Sat, 7 Oct 2017 10:38:03 +0200 Subject: [PATCH 002/105] Update file-upload.js Fixed tiny translation issues. Removed space before ellipsis. --- apps/files/js/file-upload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index b86b42bdb9..bf8116a6e1 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -971,7 +971,7 @@ OC.Uploader.prototype = _.extend({ $('#uploadprogressbar').progressbar({value: 0}); $('#uploadprogressbar .ui-progressbar-value'). html('' - + t('files', 'Uploading …') + + t('files', 'Uploading…') + '' + t('files', '…') + ''); @@ -1023,7 +1023,7 @@ OC.Uploader.prototype = _.extend({ var h = moment.duration(smoothRemainingSeconds, "seconds").humanize(); if (!(smoothRemainingSeconds >= 0 && smoothRemainingSeconds < 14400)) { // show "Uploading ..." for durations longer than 4 hours - h = t('files', 'Uploading …'); + h = t('files', 'Uploading…'); } $('#uploadprogressbar .label .mobile').text(h); $('#uploadprogressbar .label .desktop').text(h); From bdb0265644b43ba807dcc4ed1655fde6bb8c6174 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 16 Oct 2017 18:31:43 +0200 Subject: [PATCH 003/105] Fix missing translation of "Personal" calendar in activities Signed-off-by: Joas Schilling --- apps/dav/lib/CalDAV/Activity/Backend.php | 66 +++++++++++++------ .../dav/lib/CalDAV/Activity/Provider/Base.php | 28 +++++++- .../lib/CalDAV/Activity/Provider/Calendar.php | 56 ++++++++++++++-- .../lib/CalDAV/Activity/Provider/Event.php | 26 +++++++- .../dav/lib/CalDAV/Activity/Provider/Todo.php | 30 ++++++++- .../unit/CalDAV/Activity/BackendTest.php | 8 +++ .../CalDAV/Activity/Provider/BaseTest.php | 36 +++++++++- 7 files changed, 217 insertions(+), 33 deletions(-) diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php index f8cc82407f..c1a68c1682 100644 --- a/apps/dav/lib/CalDAV/Activity/Backend.php +++ b/apps/dav/lib/CalDAV/Activity/Backend.php @@ -135,8 +135,12 @@ class Backend { ->setSubject( $user === $currentUser ? $action . '_self' : $action, [ - $currentUser, - $calendarData['{DAV:}displayname'], + 'actor' => $currentUser, + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], ] ); $this->activityManager->publish($event); @@ -187,8 +191,13 @@ class Backend { if ($owner !== $principal[2]) { $parameters = [ - $principal[2], - $calendarData['{DAV:}displayname'], + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], + 'user' => $principal[2], ]; if ($owner === $event->getAuthor()) { @@ -201,7 +210,6 @@ class Backend { $this->activityManager->publish($event); $subject = Calendar::SUBJECT_UNSHARE_USER . '_by'; - $parameters[] = $event->getAuthor(); } $event->setAffectedUser($owner) @@ -212,8 +220,13 @@ class Backend { $this->triggerActivityGroup($principal[2], $event, $calendarData, Calendar::SUBJECT_UNSHARE_USER); $parameters = [ - $principal[2], - $calendarData['{DAV:}displayname'], + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], + 'group' => $principal[2], ]; if ($owner === $event->getAuthor()) { @@ -224,7 +237,6 @@ class Backend { $this->activityManager->publish($event); $subject = Calendar::SUBJECT_UNSHARE_GROUP . '_by'; - $parameters[] = $event->getAuthor(); } $event->setAffectedUser($owner) @@ -250,8 +262,13 @@ class Backend { if ($owner !== $principal[2]) { $parameters = [ - $principal[2], - $calendarData['{DAV:}displayname'], + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], + 'user' => $principal[2], ]; if ($owner === $event->getAuthor()) { @@ -262,7 +279,6 @@ class Backend { $this->activityManager->publish($event); $subject = Calendar::SUBJECT_SHARE_USER . '_by'; - $parameters[] = $event->getAuthor(); } $event->setAffectedUser($owner) @@ -273,8 +289,13 @@ class Backend { $this->triggerActivityGroup($principal[2], $event, $calendarData, Calendar::SUBJECT_SHARE_USER); $parameters = [ - $principal[2], - $calendarData['{DAV:}displayname'], + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], + 'group' => $principal[2], ]; if ($owner === $event->getAuthor()) { @@ -285,7 +306,6 @@ class Backend { $this->activityManager->publish($event); $subject = Calendar::SUBJECT_SHARE_GROUP . '_by'; - $parameters[] = $event->getAuthor(); } $event->setAffectedUser($owner) @@ -347,8 +367,12 @@ class Backend { ->setSubject( $user === $event->getAuthor() && $subjectSelf ? $subjectSelf : $subject, [ - $event->getAuthor(), - $properties['{DAV:}displayname'], + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $properties['id'], + 'uri' => $properties['uri'], + 'name' => $properties['{DAV:}displayname'], + ], ] ); @@ -401,9 +425,13 @@ class Backend { ->setSubject( $user === $currentUser ? $action . '_self' : $action, [ - $currentUser, - $calendarData['{DAV:}displayname'], - [ + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], + 'object' => [ 'id' => $object['id'], 'name' => $object['name'], ], diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php index 72fdd681b8..983d05310a 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php @@ -21,8 +21,10 @@ namespace OCA\DAV\CalDAV\Activity\Provider; +use OCA\DAV\CalDAV\CalDavBackend; use OCP\Activity\IEvent; use OCP\Activity\IProvider; +use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -64,7 +66,7 @@ abstract class Base implements IProvider { protected function generateObjectParameter($eventData) { if (!is_array($eventData) || !isset($eventData['id']) || !isset($eventData['name'])) { throw new \InvalidArgumentException(); - }; + } return [ 'type' => 'calendar-event', @@ -73,12 +75,34 @@ abstract class Base implements IProvider { ]; } + /** + * @param array $data + * @param IL10N $l + * @return array + */ + protected function generateCalendarParameter($data, IL10N $l) { + if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI && + $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) { + return [ + 'type' => 'calendar', + 'id' => $data['id'], + 'name' => $l->t('Personal'), + ]; + } + + return [ + 'type' => 'calendar', + 'id' => $data['id'], + 'name' => $data['name'], + ]; + } + /** * @param int $id * @param string $name * @return array */ - protected function generateCalendarParameter($id, $name) { + protected function generateLegacyCalendarParameter($id, $name) { return [ 'type' => 'calendar', 'id' => $id, diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php index 36d425ecf6..1efdbb13b8 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php @@ -156,6 +156,52 @@ class Calendar extends Base { $subject = $event->getSubject(); $parameters = $event->getSubjectParameters(); + // Nextcloud 13+ + if (isset($parameters['calendar'])) { + switch ($subject) { + case self::SUBJECT_ADD: + case self::SUBJECT_ADD . '_self': + case self::SUBJECT_DELETE: + case self::SUBJECT_DELETE . '_self': + case self::SUBJECT_UPDATE: + case self::SUBJECT_UPDATE . '_self': + case self::SUBJECT_SHARE_USER: + case self::SUBJECT_UNSHARE_USER: + case self::SUBJECT_UNSHARE_USER . '_self': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + ]; + case self::SUBJECT_SHARE_USER . '_you': + case self::SUBJECT_UNSHARE_USER . '_you': + return [ + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'user' => $this->generateUserParameter($parameters['user']), + ]; + case self::SUBJECT_SHARE_USER . '_by': + case self::SUBJECT_UNSHARE_USER . '_by': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'user' => $this->generateUserParameter($parameters['user']), + ]; + case self::SUBJECT_SHARE_GROUP . '_you': + case self::SUBJECT_UNSHARE_GROUP . '_you': + return [ + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'group' => $this->generateGroupParameter($parameters['group']), + ]; + case self::SUBJECT_SHARE_GROUP . '_by': + case self::SUBJECT_UNSHARE_GROUP . '_by': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'group' => $this->generateGroupParameter($parameters['group']), + ]; + } + } + + // Legacy switch ($subject) { case self::SUBJECT_ADD: case self::SUBJECT_ADD . '_self': @@ -168,32 +214,32 @@ class Calendar extends Base { case self::SUBJECT_UNSHARE_USER . '_self': return [ 'actor' => $this->generateUserParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), ]; case self::SUBJECT_SHARE_USER . '_you': case self::SUBJECT_UNSHARE_USER . '_you': return [ 'user' => $this->generateUserParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), ]; case self::SUBJECT_SHARE_USER . '_by': case self::SUBJECT_UNSHARE_USER . '_by': return [ 'user' => $this->generateUserParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), 'actor' => $this->generateUserParameter($parameters[2]), ]; case self::SUBJECT_SHARE_GROUP . '_you': case self::SUBJECT_UNSHARE_GROUP . '_you': return [ 'group' => $this->generateGroupParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), ]; case self::SUBJECT_SHARE_GROUP . '_by': case self::SUBJECT_UNSHARE_GROUP . '_by': return [ 'group' => $this->generateGroupParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), 'actor' => $this->generateUserParameter($parameters[2]), ]; } diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php index 2c2e3d01c2..dafd2404d2 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php @@ -118,20 +118,42 @@ class Event extends Base { $subject = $event->getSubject(); $parameters = $event->getSubjectParameters(); + // Nextcloud 13+ + if (isset($parameters['calendar'])) { + switch ($subject) { + case self::SUBJECT_OBJECT_ADD . '_event': + case self::SUBJECT_OBJECT_DELETE . '_event': + case self::SUBJECT_OBJECT_UPDATE . '_event': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'event' => $this->generateObjectParameter($parameters['object']), + ]; + case self::SUBJECT_OBJECT_ADD . '_event_self': + case self::SUBJECT_OBJECT_DELETE . '_event_self': + case self::SUBJECT_OBJECT_UPDATE . '_event_self': + return [ + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'event' => $this->generateObjectParameter($parameters['object']), + ]; + } + } + + // Legacy switch ($subject) { case self::SUBJECT_OBJECT_ADD . '_event': case self::SUBJECT_OBJECT_DELETE . '_event': case self::SUBJECT_OBJECT_UPDATE . '_event': return [ 'actor' => $this->generateUserParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), 'event' => $this->generateObjectParameter($parameters[2]), ]; case self::SUBJECT_OBJECT_ADD . '_event_self': case self::SUBJECT_OBJECT_DELETE . '_event_self': case self::SUBJECT_OBJECT_UPDATE . '_event_self': return [ - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), 'event' => $this->generateObjectParameter($parameters[2]), ]; } diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php index a665caa0e6..0f9cd64053 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php @@ -87,6 +87,32 @@ class Todo extends Event { $subject = $event->getSubject(); $parameters = $event->getSubjectParameters(); + // Nextcloud 13+ + if (isset($parameters['calendar'])) { + switch ($subject) { + case self::SUBJECT_OBJECT_ADD . '_todo': + case self::SUBJECT_OBJECT_DELETE . '_todo': + case self::SUBJECT_OBJECT_UPDATE . '_todo': + case self::SUBJECT_OBJECT_UPDATE . '_todo_completed': + case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'todo' => $this->generateObjectParameter($parameters['object']), + ]; + case self::SUBJECT_OBJECT_ADD . '_todo_self': + case self::SUBJECT_OBJECT_DELETE . '_todo_self': + case self::SUBJECT_OBJECT_UPDATE . '_todo_self': + case self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self': + case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self': + return [ + 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), + 'todo' => $this->generateObjectParameter($parameters['object']), + ]; + } + } + + // Legacy switch ($subject) { case self::SUBJECT_OBJECT_ADD . '_todo': case self::SUBJECT_OBJECT_DELETE . '_todo': @@ -95,7 +121,7 @@ class Todo extends Event { case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action': return [ 'actor' => $this->generateUserParameter($parameters[0]), - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), 'todo' => $this->generateObjectParameter($parameters[2]), ]; case self::SUBJECT_OBJECT_ADD . '_todo_self': @@ -104,7 +130,7 @@ class Todo extends Event { case self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self': case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self': return [ - 'calendar' => $this->generateCalendarParameter((int)$event->getObjectId(), $parameters[1]), + 'calendar' => $this->generateLegacyCalendarParameter((int)$event->getObjectId(), $parameters[1]), 'todo' => $this->generateObjectParameter($parameters[2]), ]; } diff --git a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php index d8d15e8f1f..dd48d8172d 100644 --- a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php +++ b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php @@ -108,11 +108,13 @@ class BackendTest extends TestCase { [Calendar::SUBJECT_ADD, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], [], [], '', 'admin', null, ['admin']], [Calendar::SUBJECT_ADD, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], [], [], 'test2', 'test2', null, ['admin']], @@ -122,17 +124,20 @@ class BackendTest extends TestCase { [Calendar::SUBJECT_UPDATE, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], ['shares'], [], '', 'admin', null, ['admin']], // Visible change [Calendar::SUBJECT_UPDATE, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']], [Calendar::SUBJECT_UPDATE, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']], @@ -141,16 +146,19 @@ class BackendTest extends TestCase { [Calendar::SUBJECT_DELETE, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], ['shares'], [], '', 'admin', [], ['admin']], [Calendar::SUBJECT_DELETE, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']], [Calendar::SUBJECT_DELETE, [ 'principaluri' => 'principal/user/admin', 'id' => 42, + 'uri' => 'this-uri', '{DAV:}displayname' => 'Name of calendar', ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']], ]; diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php index 85eb439f10..adffaf27de 100644 --- a/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php +++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php @@ -24,6 +24,7 @@ namespace OCA\DAV\Tests\unit\CalDAV\Activity\Provider; use OCA\DAV\CalDAV\Activity\Provider\Base; use OCP\Activity\IEvent; use OCP\Activity\IProvider; +use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; use Test\TestCase; @@ -112,6 +113,35 @@ class BaseTest extends TestCase { } public function dataGenerateCalendarParameter() { + return [ + [['id' => 23, 'uri' => 'foo', 'name' => 'bar'], 'bar'], + [['id' => 42, 'uri' => 'foo', 'name' => 'Personal'], 'Personal'], + [['id' => 42, 'uri' => 'personal', 'name' => 'bar'], 'bar'], + [['id' => 42, 'uri' => 'personal', 'name' => 'Personal'], 't(Personal)'], + ]; + } + + /** + * @dataProvider dataGenerateCalendarParameter + * @param array $data + * @param string $name + */ + public function testGenerateCalendarParameter(array $data, $name) { + $l = $this->createMock(IL10N::class); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function($string, $args) { + return 't(' . vsprintf($string, $args) . ')'; + }); + + $this->assertEquals([ + 'type' => 'calendar', + 'id' => $data['id'], + 'name' => $name, + ], $this->invokePrivate($this->provider, 'generateCalendarParameter', [$data, $l])); + } + + public function dataGenerateLegacyCalendarParameter() { return [ [23, 'c1'], [42, 'c2'], @@ -119,16 +149,16 @@ class BaseTest extends TestCase { } /** - * @dataProvider dataGenerateCalendarParameter + * @dataProvider dataGenerateLegacyCalendarParameter * @param int $id * @param string $name */ - public function testGenerateCalendarParameter($id, $name) { + public function testGenerateLegacyCalendarParameter($id, $name) { $this->assertEquals([ 'type' => 'calendar', 'id' => $id, 'name' => $name, - ], $this->invokePrivate($this->provider, 'generateCalendarParameter', [$id, $name])); + ], $this->invokePrivate($this->provider, 'generateLegacyCalendarParameter', [$id, $name])); } public function dataGenerateGroupParameter() { From ce27e8cf34daea23117a9e5e0134957889aac332 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 18 Oct 2017 15:39:22 +0200 Subject: [PATCH 004/105] Add attachment support to emails Signed-off-by: Joas Schilling --- lib/composer/composer/autoload_classmap.php | 2 + lib/composer/composer/autoload_static.php | 2 + lib/private/Mail/Attachment.php | 78 +++++++++++++++++++++ lib/private/Mail/Message.php | 20 ++++++ lib/public/Mail/IAttachment.php | 53 ++++++++++++++ lib/public/Mail/IMessage.php | 16 ++++- 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 lib/private/Mail/Attachment.php create mode 100644 lib/public/Mail/IAttachment.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 489b9b3dc1..4f3326e2d7 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -224,6 +224,7 @@ return array( 'OCP\\Lock\\ILockingProvider' => $baseDir . '/lib/public/Lock/ILockingProvider.php', 'OCP\\Lock\\LockedException' => $baseDir . '/lib/public/Lock/LockedException.php', 'OCP\\Lockdown\\ILockdownManager' => $baseDir . '/lib/public/Lockdown/ILockdownManager.php', + 'OCP\\Mail\\IAttachment' => $baseDir . '/lib/public/Mail/IAttachment.php', 'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php', 'OCP\\Mail\\IMessage' => $baseDir . '/lib/public/Mail/IMessage.php', @@ -705,6 +706,7 @@ return array( 'OC\\Log\\File' => $baseDir . '/lib/private/Log/File.php', 'OC\\Log\\Rotate' => $baseDir . '/lib/private/Log/Rotate.php', 'OC\\Log\\Syslog' => $baseDir . '/lib/private/Log/Syslog.php', + 'OC\\Mail\\Attachment' => $baseDir . '/lib/private/Mail/Attachment.php', 'OC\\Mail\\EMailTemplate' => $baseDir . '/lib/private/Mail/EMailTemplate.php', 'OC\\Mail\\Mailer' => $baseDir . '/lib/private/Mail/Mailer.php', 'OC\\Mail\\Message' => $baseDir . '/lib/private/Mail/Message.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1297f4606f..b9c5c00416 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -254,6 +254,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Lock\\ILockingProvider' => __DIR__ . '/../../..' . '/lib/public/Lock/ILockingProvider.php', 'OCP\\Lock\\LockedException' => __DIR__ . '/../../..' . '/lib/public/Lock/LockedException.php', 'OCP\\Lockdown\\ILockdownManager' => __DIR__ . '/../../..' . '/lib/public/Lockdown/ILockdownManager.php', + 'OCP\\Mail\\IAttachment' => __DIR__ . '/../../..' . '/lib/public/Mail/IAttachment.php', 'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php', 'OCP\\Mail\\IMessage' => __DIR__ . '/../../..' . '/lib/public/Mail/IMessage.php', @@ -735,6 +736,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Log\\File' => __DIR__ . '/../../..' . '/lib/private/Log/File.php', 'OC\\Log\\Rotate' => __DIR__ . '/../../..' . '/lib/private/Log/Rotate.php', 'OC\\Log\\Syslog' => __DIR__ . '/../../..' . '/lib/private/Log/Syslog.php', + 'OC\\Mail\\Attachment' => __DIR__ . '/../../..' . '/lib/private/Mail/Attachment.php', 'OC\\Mail\\EMailTemplate' => __DIR__ . '/../../..' . '/lib/private/Mail/EMailTemplate.php', 'OC\\Mail\\Mailer' => __DIR__ . '/../../..' . '/lib/private/Mail/Mailer.php', 'OC\\Mail\\Message' => __DIR__ . '/../../..' . '/lib/private/Mail/Message.php', diff --git a/lib/private/Mail/Attachment.php b/lib/private/Mail/Attachment.php new file mode 100644 index 0000000000..7b85ad1dbb --- /dev/null +++ b/lib/private/Mail/Attachment.php @@ -0,0 +1,78 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Mail; + +use OCP\Mail\IAttachment; + +/** + * Class Attachment + * + * @package OC\Mail + * @since 13.0.0 + */ +class Attachment implements IAttachment { + + /** @var \Swift_Mime_Attachment */ + protected $swiftAttachment; + + public function __construct(\Swift_Mime_Attachment $attachment) { + $this->swiftAttachment = $attachment; + } + + /** + * @param string $filename + * @return $this + * @since 13.0.0 + */ + public function setFilename($filename) { + $this->swiftAttachment->setFilename($filename); + return $this; + } + + /** + * @param string $contentType + * @return $this + * @since 13.0.0 + */ + public function setContentType($contentType) { + $this->swiftAttachment->setContentType($contentType); + return $this; + } + + /** + * @param string $body + * @return $this + * @since 13.0.0 + */ + public function setBody($body) { + $this->swiftAttachment->setBody($body); + return $this; + } + + /** + * @return \Swift_Mime_Attachment + */ + public function getSwiftAttachment() { + return $this->swiftAttachment; + } + +} diff --git a/lib/private/Mail/Message.php b/lib/private/Mail/Message.php index b4d1e4dbe7..35c2c2d84e 100644 --- a/lib/private/Mail/Message.php +++ b/lib/private/Mail/Message.php @@ -23,6 +23,7 @@ namespace OC\Mail; +use OCP\Mail\IAttachment; use OCP\Mail\IEMailTemplate; use OCP\Mail\IMessage; use Swift_Message; @@ -43,6 +44,25 @@ class Message implements IMessage { $this->swiftMessage = $swiftMessage; } + /** + * @return IAttachment + * @since 13.0.0 + */ + public function createAttachment() { + return new Attachment(\Swift_Attachment::newInstance()); + } + + /** + * @param IAttachment $attachment + * @return $this + * @since 13.0.0 + */ + public function attach(IAttachment $attachment) { + /** @var Attachment $attachment */ + $this->swiftMessage->attach($attachment->getSwiftAttachment()); + return $this; + } + /** * SwiftMailer does currently not work with IDN domains, this function therefore converts the domains * FIXME: Remove this once SwiftMailer supports IDN diff --git a/lib/public/Mail/IAttachment.php b/lib/public/Mail/IAttachment.php new file mode 100644 index 0000000000..32348e7a30 --- /dev/null +++ b/lib/public/Mail/IAttachment.php @@ -0,0 +1,53 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Mail; + +/** + * Interface IAttachment + * + * @package OCP\Mail + * @since 13.0.0 + */ +interface IAttachment { + + /** + * @param string $filename + * @return $this + * @since 13.0.0 + */ + public function setFilename($filename); + + /** + * @param string $contentType + * @return $this + * @since 13.0.0 + */ + public function setContentType($contentType); + + /** + * @param string $body + * @return $this + * @since 13.0.0 + */ + public function setBody($body); + +} diff --git a/lib/public/Mail/IMessage.php b/lib/public/Mail/IMessage.php index 20e4ea19c4..7f061cece0 100644 --- a/lib/public/Mail/IMessage.php +++ b/lib/public/Mail/IMessage.php @@ -22,12 +22,26 @@ namespace OCP\Mail; /** - * Class Message + * Interface IMessage * * @package OCP\Mail * @since 13.0.0 */ interface IMessage { + + /** + * @return IAttachment + * @since 13.0.0 + */ + public function createAttachment(); + + /** + * @param IAttachment $attachment + * @return $this + * @since 13.0.0 + */ + public function attach(IAttachment $attachment); + /** * Set the from address of this message. * From a8f1902b02d02cc2b7d624b3d7b288f8aea84fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 27 Sep 2017 01:50:24 +0200 Subject: [PATCH 005/105] Add support to FileActionsMenu for icon class functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icon class function properties make possible to render a different icon class depending on the context of the file action. Inline file actions had support for them already and called them passing the file name and context of the file action as parameters. Due to this the FileActionsMenu passes those parameters too to icon class functions instead of just the context like done for display name functions. Signed-off-by: Daniel Calviño Sánchez --- apps/files/js/fileactions.js | 13 +++++++++- apps/files/js/fileactionsmenu.js | 5 ++++ apps/files/tests/js/fileactionsmenuSpec.js | 28 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 3da9b06b0d..0f320c8b3c 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -706,7 +706,7 @@ * @property {String} mime mime type * @property {int} permissions permissions * @property {(Function|String)} icon icon path to the icon or function that returns it (deprecated, use iconClass instead) - * @property {(Function|String)} iconClass class name of the icon (recommended for theming) + * @property {(String|OCA.Files.FileActions~iconClassFunction)} iconClass class name of the icon (recommended for theming) * @property {OCA.Files.FileActions~renderActionFunction} [render] optional rendering function * @property {OCA.Files.FileActions~actionHandler} actionHandler action handler function */ @@ -745,6 +745,17 @@ * @return {String} display name */ + /** + * Icon class function for actions. + * The function returns the icon class of the action using + * the given context information. + * + * @callback OCA.Files.FileActions~iconClassFunction + * @param {String} fileName name of the file on which the action must be performed + * @param {OCA.Files.FileActionContext} context action context + * @return {String} icon class + */ + /** * Action handler function for file actions * diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js index 45d2bd8304..b8022f1373 100644 --- a/apps/files/js/fileactionsmenu.js +++ b/apps/files/js/fileactionsmenu.js @@ -115,6 +115,11 @@ item = _.extend({}, item); item.displayName = item.displayName(self._context); } + if (_.isFunction(item.iconClass)) { + var fileName = self._context.$file.attr('data-file'); + item = _.extend({}, item); + item.iconClass = item.iconClass(fileName, self._context); + } return item; }); items = items.sort(function(actionA, actionB) { diff --git a/apps/files/tests/js/fileactionsmenuSpec.js b/apps/files/tests/js/fileactionsmenuSpec.js index 3028db2b3a..926516b304 100644 --- a/apps/files/tests/js/fileactionsmenuSpec.js +++ b/apps/files/tests/js/fileactionsmenuSpec.js @@ -205,6 +205,34 @@ describe('OCA.Files.FileActionsMenu tests', function() { expect(displayNameStub.calledWith(menuContext)).toEqual(true); expect(menu.$el.find('a[data-action=Something]').text()).toEqual('Test'); }); + it('uses plain iconClass', function() { + fileActions.registerAction({ + name: 'Something', + mime: 'text/plain', + permissions: OC.PERMISSION_ALL, + iconClass: 'test' + }); + + menu.render(); + + expect(menu.$el.find('a[data-action=Something]').children('span.icon').hasClass('test')).toEqual(true); + }); + it('calls iconClass function', function() { + var iconClassStub = sinon.stub().returns('test'); + + fileActions.registerAction({ + name: 'Something', + mime: 'text/plain', + permissions: OC.PERMISSION_ALL, + iconClass: iconClassStub + }); + + menu.render(); + + expect(iconClassStub.calledOnce).toEqual(true); + expect(iconClassStub.calledWith(menuContext.$file.attr('data-file'), menuContext)).toEqual(true); + expect(menu.$el.find('a[data-action=Something]').children('span.icon').hasClass('test')).toEqual(true); + }); }); describe('action handler', function() { From 420d1b91ab0d3c9e521bf6def682fd3c58fde997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 27 Sep 2017 08:07:20 +0200 Subject: [PATCH 006/105] Add "Favorite" action to the file actions menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new FileAction for the menu is essentially the same as the old inline FileAction, except for the rendering; in this case the FileAction is shown in the menu in a standard way, so there is no need to provide a custom renderer (although the menu entry text and icon change depending on whether the file is currently a favorite or not, but that can be done just with displayName and iconClass functions). Signed-off-by: Daniel Calviño Sánchez --- apps/files/css/files.scss | 2 +- apps/files/js/tagsplugin.js | 75 ++++++++++++++++++++++++++- apps/files/tests/js/tagspluginspec.js | 56 ++++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 7c2d3b0bb1..4a59502bc6 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -502,7 +502,7 @@ table td.filename .uploadtext { display: inline-block; float: left; } -#fileList tr td.filename .action-favorite { +#fileList tr td.filename .action-favorite:not(.menuitem) { display: block; float: left; width: 30px; diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 9bd20be4bf..22d04e9f19 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -86,7 +86,7 @@ var self = this; // register "star" action fileActions.registerAction({ - name: 'Favorite', + name: 'FavoriteInline', displayName: t('files', 'Favorite'), mime: 'all', permissions: OC.PERMISSION_READ, @@ -141,6 +141,79 @@ }); } }); + + fileActions.registerAction({ + name: 'Favorite', + displayName: function(context) { + var $file = context.$file; + var isFavorite = $file.data('favorite') === true; + + if (isFavorite) { + return t('files', 'Remove from favorites'); + } + + // As it is currently not possible to provide a context for + // the i18n strings "Add to favorites" was used instead of + // "Favorite" to remove the ambiguity between verb and noun + // when it is translated. + return t('files', 'Add to favorites'); + }, + mime: 'all', + order: -23, + permissions: OC.PERMISSION_READ, + iconClass: function(fileName, context) { + var $file = context.$file; + var isFavorite = $file.data('favorite') === true; + + if (isFavorite) { + return 'icon-starred'; + } + + return 'icon-star'; + }, + actionHandler: function(fileName, context) { + var $actionEl = context.$file.find('.action-favorite'); + var $file = context.$file; + var fileInfo = context.fileList.files[$file.index()]; + var dir = context.dir || context.fileList.getCurrentDirectory(); + var tags = $file.attr('data-tags'); + if (_.isUndefined(tags)) { + tags = ''; + } + tags = tags.split('|'); + tags = _.without(tags, ''); + var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0; + if (isFavorite) { + // remove tag from list + tags = _.without(tags, OC.TAG_FAVORITE); + } else { + tags.push(OC.TAG_FAVORITE); + } + + // pre-toggle the star + toggleStar($actionEl, !isFavorite); + + context.fileInfoModel.trigger('busy', context.fileInfoModel, true); + + self.applyFileTags( + dir + '/' + fileName, + tags, + $actionEl, + isFavorite + ).then(function(result) { + context.fileInfoModel.trigger('busy', context.fileInfoModel, false); + // response from server should contain updated tags + var newTags = result.tags; + if (_.isUndefined(newTags)) { + newTags = tags; + } + context.fileInfoModel.set({ + 'tags': newTags, + 'favorite': !isFavorite + }); + }); + } + }); }, _extendFileList: function(fileList) { diff --git a/apps/files/tests/js/tagspluginspec.js b/apps/files/tests/js/tagspluginspec.js index a4efc08aa5..e2cbaa2c4d 100644 --- a/apps/files/tests/js/tagspluginspec.js +++ b/apps/files/tests/js/tagspluginspec.js @@ -117,6 +117,62 @@ describe('OCA.Files.TagsPlugin tests', function() { $tr = fileList.findFileEl('One.txt'); $action = $tr.find('.action-favorite'); + expect($tr.attr('data-favorite')).toBeFalsy(); + expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3']); + expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3']); + expect($action.find('.icon').hasClass('icon-star')).toEqual(true); + expect($action.find('.icon').hasClass('icon-starred')).toEqual(false); + }); + it('through FileActionsMenu sends request to server and updates icon', function() { + var request; + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + var $action = $tr.find('.action-favorite'); + var $showMenuAction = $tr.find('.action-menu'); + $showMenuAction.click(); + var $favoriteActionInMenu = $tr.find('.fileActionsMenu .action-favorite'); + $favoriteActionInMenu.click(); + + expect(fakeServer.requests.length).toEqual(1); + request = fakeServer.requests[0]; + expect(JSON.parse(request.requestBody)).toEqual({ + tags: ['tag1', 'tag2', OC.TAG_FAVORITE] + }); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + tags: ['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE] + })); + + // re-read the element as it was re-inserted + $tr = fileList.findFileEl('One.txt'); + $action = $tr.find('.action-favorite'); + $showMenuAction = $tr.find('.action-menu'); + + expect($tr.attr('data-favorite')).toEqual('true'); + expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]); + expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]); + expect($action.find('.icon').hasClass('icon-star')).toEqual(false); + expect($action.find('.icon').hasClass('icon-starred')).toEqual(true); + + // show again the menu and get the new action, as the menu was + // closed and removed (and with it, the previous action) when that + // action was clicked + $showMenuAction.click(); + $favoriteActionInMenu = $tr.find('.fileActionsMenu .action-favorite'); + $favoriteActionInMenu.click(); + + expect(fakeServer.requests.length).toEqual(2); + request = fakeServer.requests[1]; + expect(JSON.parse(request.requestBody)).toEqual({ + tags: ['tag1', 'tag2', 'tag3'] + }); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + tags: ['tag1', 'tag2', 'tag3'] + })); + + // re-read the element as it was re-inserted + $tr = fileList.findFileEl('One.txt'); + $action = $tr.find('.action-favorite'); + expect($tr.attr('data-favorite')).toBeFalsy(); expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3']); expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3']); From 0e933ee6b9d6598eec1e830b90becb8dcdd4967a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 27 Sep 2017 11:17:05 +0200 Subject: [PATCH 007/105] Update acceptance tests to favorite files through the file actions menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently a file can be favorited either through the inline action or through the file actions menu. However, the inline action will be removed in a following commit and then it will be possible to do it only through the file actions menu. Signed-off-by: Daniel Calviño Sánchez --- .../features/bootstrap/FilesAppContext.php | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php index bb088c0a2c..a2b286330f 100644 --- a/tests/acceptance/features/bootstrap/FilesAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppContext.php @@ -338,6 +338,20 @@ class FilesAppContext implements Context, ActorAwareInterface { return self::fileActionsMenuItemFor("Details"); } + /** + * @return Locator + */ + public static function addToFavoritesMenuItem() { + return self::fileActionsMenuItemFor("Add to favorites"); + } + + /** + * @return Locator + */ + public static function removeFromFavoritesMenuItem() { + return self::fileActionsMenuItemFor("Remove from favorites"); + } + /** * @return Locator */ @@ -393,7 +407,9 @@ class FilesAppContext implements Context, ActorAwareInterface { public function iMarkAsFavorite($fileName) { $this->iSeeThatIsNotMarkedAsFavorite($fileName); - $this->actor->find(self::favoriteActionForFile($fileName), 10)->click(); + $this->actor->find(self::fileActionsMenuButtonForFile($fileName), 10)->click(); + + $this->actor->find(self::addToFavoritesMenuItem(), 2)->click(); } /** @@ -402,7 +418,9 @@ class FilesAppContext implements Context, ActorAwareInterface { public function iUnmarkAsFavorite($fileName) { $this->iSeeThatIsMarkedAsFavorite($fileName); - $this->actor->find(self::favoriteActionForFile($fileName), 10)->click(); + $this->actor->find(self::fileActionsMenuButtonForFile($fileName), 10)->click(); + + $this->actor->find(self::removeFromFavoritesMenuItem(), 2)->click(); } /** From 9ff0941c076c4f9c96872bdab8f97568793a8f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 27 Sep 2017 12:39:59 +0200 Subject: [PATCH 008/105] Replace inline favorite action with just a favorite icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a preparatory step for a following commit in which the position of the favorite icon and the checkbox will be swapped; in that new design the favorite icon is no longer expected to be an action but just a simple mark on whether the file is favorited or not (the action is expected to be triggered then only from the file actions menu). The favorite icon is now fully shown or completely hidden depending on whether the file is favorited or not. As the icon is just informative but no longer an action now it does not change when hovered or focus. In the same way, the alternative text when the file is not favorited now it is not "Favorite" (an action) but "Not favorited" instead. Signed-off-by: Daniel Calviño Sánchez --- apps/files/css/files.scss | 19 +++- apps/files/js/tagsplugin.js | 101 +++++------------- apps/files/tests/js/tagspluginspec.js | 77 +++---------- .../features/bootstrap/FilesAppContext.php | 10 +- 4 files changed, 60 insertions(+), 147 deletions(-) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 4a59502bc6..f5cb130cfe 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -502,7 +502,7 @@ table td.filename .uploadtext { display: inline-block; float: left; } -#fileList tr td.filename .action-favorite:not(.menuitem) { +#fileList tr td.filename .favorite-mark { display: block; float: left; width: 30px; @@ -569,7 +569,8 @@ a.action > img { margin-bottom: -1px; } -#fileList a.action { +#fileList a.action, +#fileList div.favorite-mark { display: inline; padding: 17px 8px; line-height: 50px; @@ -617,7 +618,7 @@ a.action > img { padding-left: 6px; } -#fileList .action.action-favorite.permanent { +#fileList .favorite-mark.permanent { opacity: 1; } @@ -716,12 +717,24 @@ table.dragshadow td.size { #filestable .filename .action .icon, #filestable .selectedActions a .icon, +#filestable .filename .favorite-mark .icon, #controls .actions .button .icon { display: inline-block; vertical-align: middle; background-size: 16px 16px; } +#filestable .filename .favorite-mark { + // Override default icons to always hide the star icon and always show the + // starred icon even when hovered or focused. + & .icon-star { + background-image: none; + } + & .icon-starred { + background-image: url('../../../core/img/actions/starred.svg?v=1'); + } +} + #filestable .filename .action .icon.hidden, #filestable .selectedActions a .icon.hidden, #controls .actions .button .icon.hidden { diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 22d04e9f19..e6724e371c 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -17,12 +17,12 @@ PROPERTY_FAVORITE: '{' + OC.Files.Client.NS_OWNCLOUD + '}favorite' }); - var TEMPLATE_FAVORITE_ACTION = - '' + + var TEMPLATE_FAVORITE_MARK = + ''; /** * Returns the icon class for the matching state @@ -42,24 +42,24 @@ */ function renderStar(state) { if (!this._template) { - this._template = Handlebars.compile(TEMPLATE_FAVORITE_ACTION); + this._template = Handlebars.compile(TEMPLATE_FAVORITE_MARK); } return this._template({ isFavorite: state, - altText: state ? t('files', 'Favorited') : t('files', 'Favorite'), + altText: state ? t('files', 'Favorited') : t('files', 'Not favorited'), iconClass: getStarIconClass(state) }); } /** - * Toggle star icon on action element + * Toggle star icon on favorite mark element * - * @param {Object} action element + * @param {Object} $favoriteMarkEl favorite mark element * @param {boolean} state true if starred, false otherwise */ - function toggleStar($actionEl, state) { - $actionEl.removeClass('icon-star icon-starred').addClass(getStarIconClass(state)); - $actionEl.toggleClass('permanent', state); + function toggleStar($favoriteMarkEl, state) { + $favoriteMarkEl.removeClass('icon-star icon-starred').addClass(getStarIconClass(state)); + $favoriteMarkEl.toggleClass('permanent', state); } OCA.Files = OCA.Files || {}; @@ -67,8 +67,9 @@ /** * @namespace OCA.Files.TagsPlugin * - * Extends the file actions and file list to include a favorite action icon - * and addition "data-tags" and "data-favorite" attributes. + * Extends the file actions and file list to include a favorite mark icon + * and a favorite action in the file actions menu; it also adds "data-tags" + * and "data-favorite" attributes to file elements. */ OCA.Files.TagsPlugin = { name: 'Tags', @@ -84,63 +85,6 @@ _extendFileActions: function(fileActions) { var self = this; - // register "star" action - fileActions.registerAction({ - name: 'FavoriteInline', - displayName: t('files', 'Favorite'), - mime: 'all', - permissions: OC.PERMISSION_READ, - type: OCA.Files.FileActions.TYPE_INLINE, - render: function(actionSpec, isDefault, context) { - var $file = context.$file; - var isFavorite = $file.data('favorite') === true; - var $icon = $(renderStar(isFavorite)); - $file.find('td:first>.favorite').replaceWith($icon); - return $icon; - }, - actionHandler: function(fileName, context) { - var $actionEl = context.$file.find('.action-favorite'); - var $file = context.$file; - var fileInfo = context.fileList.files[$file.index()]; - var dir = context.dir || context.fileList.getCurrentDirectory(); - var tags = $file.attr('data-tags'); - if (_.isUndefined(tags)) { - tags = ''; - } - tags = tags.split('|'); - tags = _.without(tags, ''); - var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0; - if (isFavorite) { - // remove tag from list - tags = _.without(tags, OC.TAG_FAVORITE); - } else { - tags.push(OC.TAG_FAVORITE); - } - - // pre-toggle the star - toggleStar($actionEl, !isFavorite); - - context.fileInfoModel.trigger('busy', context.fileInfoModel, true); - - self.applyFileTags( - dir + '/' + fileName, - tags, - $actionEl, - isFavorite - ).then(function(result) { - context.fileInfoModel.trigger('busy', context.fileInfoModel, false); - // response from server should contain updated tags - var newTags = result.tags; - if (_.isUndefined(newTags)) { - newTags = tags; - } - context.fileInfoModel.set({ - 'tags': newTags, - 'favorite': !isFavorite - }); - }); - } - }); fileActions.registerAction({ name: 'Favorite', @@ -172,7 +116,7 @@ return 'icon-star'; }, actionHandler: function(fileName, context) { - var $actionEl = context.$file.find('.action-favorite'); + var $favoriteMarkEl = context.$file.find('.favorite-mark'); var $file = context.$file; var fileInfo = context.fileList.files[$file.index()]; var dir = context.dir || context.fileList.getCurrentDirectory(); @@ -191,14 +135,14 @@ } // pre-toggle the star - toggleStar($actionEl, !isFavorite); + toggleStar($favoriteMarkEl, !isFavorite); context.fileInfoModel.trigger('busy', context.fileInfoModel, true); self.applyFileTags( dir + '/' + fileName, tags, - $actionEl, + $favoriteMarkEl, isFavorite ).then(function(result) { context.fileInfoModel.trigger('busy', context.fileInfoModel, false); @@ -222,13 +166,16 @@ var oldCreateRow = fileList._createRow; fileList._createRow = function(fileData) { var $tr = oldCreateRow.apply(this, arguments); + var isFavorite = false; if (fileData.tags) { $tr.attr('data-tags', fileData.tags.join('|')); if (fileData.tags.indexOf(OC.TAG_FAVORITE) >= 0) { $tr.attr('data-favorite', true); + isFavorite = true; } } - $tr.find('td:first').prepend('
'); + var $icon = $(renderStar(isFavorite)); + $tr.find('td:first').prepend($icon); return $tr; }; var oldElementToFile = fileList.elementToFile; @@ -288,10 +235,10 @@ * * @param {String} fileName path to the file or folder to tag * @param {Array.} tagNames array of tag names - * @param {Object} $actionEl element + * @param {Object} $favoriteMarkEl favorite mark element * @param {boolean} isFavorite Was the item favorited before */ - applyFileTags: function(fileName, tagNames, $actionEl, isFavorite) { + applyFileTags: function(fileName, tagNames, $favoriteMarkEl, isFavorite) { var encodedPath = OC.encodePath(fileName); while (encodedPath[0] === '/') { encodedPath = encodedPath.substr(1); @@ -311,7 +258,7 @@ message = ': ' + response.responseJSON.message; } OC.Notification.show(t('files', 'An error occurred while trying to update the tags' + message), {type: 'error'}); - toggleStar($actionEl, isFavorite); + toggleStar($favoriteMarkEl, isFavorite); }); } }; diff --git a/apps/files/tests/js/tagspluginspec.js b/apps/files/tests/js/tagspluginspec.js index e2cbaa2c4d..b76b3c02dd 100644 --- a/apps/files/tests/js/tagspluginspec.js +++ b/apps/files/tests/js/tagspluginspec.js @@ -49,24 +49,24 @@ describe('OCA.Files.TagsPlugin tests', function() { describe('Favorites icon', function() { it('renders favorite icon and extra data', function() { - var $action, $tr; + var $favoriteMark, $tr; fileList.setFiles(testFiles); $tr = fileList.$el.find('tbody tr:first'); - $action = $tr.find('.action-favorite'); - expect($action.length).toEqual(1); - expect($action.hasClass('permanent')).toEqual(false); + $favoriteMark = $tr.find('.favorite-mark'); + expect($favoriteMark.length).toEqual(1); + expect($favoriteMark.hasClass('permanent')).toEqual(false); expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2']); expect($tr.attr('data-favorite')).not.toBeDefined(); }); it('renders permanent favorite icon and extra data', function() { - var $action, $tr; + var $favoriteMark, $tr; testFiles[0].tags.push(OC.TAG_FAVORITE); fileList.setFiles(testFiles); $tr = fileList.$el.find('tbody tr:first'); - $action = $tr.find('.action-favorite'); - expect($action.length).toEqual(1); - expect($action.hasClass('permanent')).toEqual(true); + $favoriteMark = $tr.find('.favorite-mark'); + expect($favoriteMark.length).toEqual(1); + expect($favoriteMark.hasClass('permanent')).toEqual(true); expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', OC.TAG_FAVORITE]); expect($tr.attr('data-favorite')).toEqual('true'); @@ -76,58 +76,11 @@ describe('OCA.Files.TagsPlugin tests', function() { }); }); describe('Applying tags', function() { - it('sends request to server and updates icon', function() { - var request; - fileList.setFiles(testFiles); - var $tr = fileList.findFileEl('One.txt'); - var $action = $tr.find('.action-favorite'); - $action.click(); - - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(JSON.parse(request.requestBody)).toEqual({ - tags: ['tag1', 'tag2', OC.TAG_FAVORITE] - }); - request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - tags: ['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE] - })); - - // re-read the element as it was re-inserted - $tr = fileList.findFileEl('One.txt'); - $action = $tr.find('.action-favorite'); - - expect($tr.attr('data-favorite')).toEqual('true'); - expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]); - expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]); - expect($action.find('.icon').hasClass('icon-star')).toEqual(false); - expect($action.find('.icon').hasClass('icon-starred')).toEqual(true); - - $action.click(); - - expect(fakeServer.requests.length).toEqual(2); - request = fakeServer.requests[1]; - expect(JSON.parse(request.requestBody)).toEqual({ - tags: ['tag1', 'tag2', 'tag3'] - }); - request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - tags: ['tag1', 'tag2', 'tag3'] - })); - - // re-read the element as it was re-inserted - $tr = fileList.findFileEl('One.txt'); - $action = $tr.find('.action-favorite'); - - expect($tr.attr('data-favorite')).toBeFalsy(); - expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3']); - expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3']); - expect($action.find('.icon').hasClass('icon-star')).toEqual(true); - expect($action.find('.icon').hasClass('icon-starred')).toEqual(false); - }); it('through FileActionsMenu sends request to server and updates icon', function() { var request; fileList.setFiles(testFiles); var $tr = fileList.findFileEl('One.txt'); - var $action = $tr.find('.action-favorite'); + var $favoriteMark = $tr.find('.favorite-mark'); var $showMenuAction = $tr.find('.action-menu'); $showMenuAction.click(); var $favoriteActionInMenu = $tr.find('.fileActionsMenu .action-favorite'); @@ -144,14 +97,14 @@ describe('OCA.Files.TagsPlugin tests', function() { // re-read the element as it was re-inserted $tr = fileList.findFileEl('One.txt'); - $action = $tr.find('.action-favorite'); + $favoriteMark = $tr.find('.favorite-mark'); $showMenuAction = $tr.find('.action-menu'); expect($tr.attr('data-favorite')).toEqual('true'); expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]); expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]); - expect($action.find('.icon').hasClass('icon-star')).toEqual(false); - expect($action.find('.icon').hasClass('icon-starred')).toEqual(true); + expect($favoriteMark.find('.icon').hasClass('icon-star')).toEqual(false); + expect($favoriteMark.find('.icon').hasClass('icon-starred')).toEqual(true); // show again the menu and get the new action, as the menu was // closed and removed (and with it, the previous action) when that @@ -171,13 +124,13 @@ describe('OCA.Files.TagsPlugin tests', function() { // re-read the element as it was re-inserted $tr = fileList.findFileEl('One.txt'); - $action = $tr.find('.action-favorite'); + $favoriteMark = $tr.find('.favorite-mark'); expect($tr.attr('data-favorite')).toBeFalsy(); expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3']); expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3']); - expect($action.find('.icon').hasClass('icon-star')).toEqual(true); - expect($action.find('.icon').hasClass('icon-starred')).toEqual(false); + expect($favoriteMark.find('.icon').hasClass('icon-star')).toEqual(true); + expect($favoriteMark.find('.icon').hasClass('icon-starred')).toEqual(false); }); }); describe('elementToFile', function() { diff --git a/tests/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php index a2b286330f..eee03973dc 100644 --- a/tests/acceptance/features/bootstrap/FilesAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppContext.php @@ -278,16 +278,16 @@ class FilesAppContext implements Context, ActorAwareInterface { /** * @return Locator */ - public static function favoriteActionForFile($fileName) { - return Locator::forThe()->css(".action-favorite")->descendantOf(self::rowForFile($fileName))-> - describedAs("Favorite action for file $fileName in Files app"); + public static function favoriteMarkForFile($fileName) { + return Locator::forThe()->css(".favorite-mark")->descendantOf(self::rowForFile($fileName))-> + describedAs("Favorite mark for file $fileName in Files app"); } /** * @return Locator */ public static function notFavoritedStateIconForFile($fileName) { - return Locator::forThe()->css(".icon-star")->descendantOf(self::favoriteActionForFile($fileName))-> + return Locator::forThe()->css(".icon-star")->descendantOf(self::favoriteMarkForFile($fileName))-> describedAs("Not favorited state icon for file $fileName in Files app"); } @@ -295,7 +295,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function favoritedStateIconForFile($fileName) { - return Locator::forThe()->css(".icon-starred")->descendantOf(self::favoriteActionForFile($fileName))-> + return Locator::forThe()->css(".icon-starred")->descendantOf(self::favoriteMarkForFile($fileName))-> describedAs("Favorited state icon for file $fileName in Files app"); } From e29bd3b743ca4e5bcd07b69fe019cb8208104b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 27 Sep 2017 13:58:35 +0200 Subject: [PATCH 009/105] Show always the checkbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checkbox is not shown always with full opacity, though, in order to reduce the visual noise (specially later, once the checkbox is moved to its own column). Signed-off-by: Daniel Calviño Sánchez --- apps/files/css/files.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index f5cb130cfe..8c9995b222 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -441,14 +441,14 @@ table td.filename .uploadtext { /* File checkboxes */ #fileList tr td.filename>.selectCheckBox + label:before { - opacity: 0; + opacity: 0.3; position: absolute; bottom: 4px; right: 0; z-index: 10; } -/* Show checkbox when hovering, checked, or selected */ +/* Show checkbox with full opacity when hovering, checked, or selected */ #fileList tr:hover td.filename>.selectCheckBox + label:before, #fileList tr:focus td.filename>.selectCheckBox + label:before, #fileList tr td.filename>.selectCheckBox:checked + label:before, From 2d709d5222bcc9c594408f1a4b6bc2abc1cc4742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 Sep 2017 14:39:21 +0200 Subject: [PATCH 010/105] Set background size for thumbnail in CSS instead of JavaScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- apps/files/css/files.scss | 1 + apps/files/js/filelist.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 8c9995b222..45c453e2b5 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -308,6 +308,7 @@ table td.filename .thumbnail { display: inline-block; width: 32px; height: 32px; + background-size: 32px; margin-left: 9px; margin-top: 9px; cursor: pointer; diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index cc23ac7397..c636544e6c 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1207,12 +1207,12 @@ td.append( '' ); } else { - td.append('
'); + td.append('
'); } var linkElem = $('').attr({ "class": "name", From 415ac9b3ac0e3f1f563b570f658d2622fee66c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 Sep 2017 14:42:37 +0200 Subject: [PATCH 011/105] Move favorite mark to the top right corner of the thumbnail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The favorite icon was shown on its own "column" (not a real column in the table, but a visual column achieved through margins and left positions). Now the icon was moved to the top right corner of the file thumbnail, and the thumbnail and file name were moved to the left to fill the space left by the "column". To keep the markup in line with its visual representation (and to ease the placing through CSS), the favorite mark is no longer prepended to the row, but appended to the thumbnail instead. In the same way, the thumbnail is no longer appended to the checkbox label, but to the link with the name of the file instead (although the checkbox is still shown at the bottom right corner of the thumbnail, and clicking on the thumbnail still selects the file). In order to show the "busy" state on a file the "icon-loading-small" CSS class is set to the parent element of the thumbnail, so the thumbnail is also wrapped now by another div with the same size and position as the label. Signed-off-by: Daniel Calviño Sánchez --- apps/files/css/files.scss | 38 ++++++++++++----------------- apps/files/js/filelist.js | 5 ++-- apps/files/js/tagsplugin.js | 2 +- apps/files/tests/js/filelistSpec.js | 8 +++--- core/search/css/results.css | 7 ------ 5 files changed, 22 insertions(+), 38 deletions(-) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 45c453e2b5..80a2b44ac1 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -232,9 +232,6 @@ table th#headerName { position: relative; height: 50px; } -.has-favorites #headerName-container { - padding-left: 50px; -} table th#headerSize, table td.filesize { text-align: right; @@ -296,7 +293,12 @@ table td.filename a.name { line-height: 50px; padding: 0; } -table td.filename label.icon-loading-small { +table td.filename .thumbnail-wrapper { + position: absolute; + width: 50px; + height: 50px; +} +table td.filename .thumbnail-wrapper.icon-loading-small { &:after { z-index: 10; } @@ -312,7 +314,6 @@ table td.filename .thumbnail { margin-left: 9px; margin-top: 9px; cursor: pointer; - float: left; position: absolute; z-index: 4; } @@ -322,12 +323,9 @@ table td.filename input.filename { margin-left: 48px; cursor: text; } -.has-favorites table td.filename input.filename { - margin-left: 52px; -} table td.filename a, table td.login, table td.logout, table td.download, table td.upload, table td.create, table td.delete { padding:3px 8px 8px 3px; } -table td.filename .nametext, .uploadtext, .modified, .column-last>span:first-child { float:left; padding:15px 0; } +table td.filename .nametext, .modified, .column-last>span:first-child { float:left; padding:15px 0; } .modified, .column-last>span:first-child { position: relative; @@ -347,14 +345,14 @@ table td.filename .nametext { max-width: 800px; height: 100%; } +table td.filename .uploadtext { + position: absolute; + left: 55px; +} /* ellipsis on file names */ table td.filename .nametext .innernametext { max-width: calc(100% - 100px) !important; } -.has-favorites #fileList td.filename a.name { - left: 50px; - margin-right: 50px; -} .hide-hidden-files #fileList tr.hidden-file, .hide-hidden-files #fileList tr.hidden-file.dragging { @@ -481,9 +479,6 @@ table td.filename .uploadtext { left: 18px; z-index: 10; } -.has-favorites .select-all { - left: 68px; -} #fileList tr td.filename { position: relative; @@ -504,9 +499,10 @@ table td.filename .uploadtext { float: left; } #fileList tr td.filename .favorite-mark { + position: absolute; display: block; - float: left; - width: 30px; + top: -6px; + right: -6px; line-height: 100%; text-align: center; } @@ -570,8 +566,7 @@ a.action > img { margin-bottom: -1px; } -#fileList a.action, -#fileList div.favorite-mark { +#fileList a.action { display: inline; padding: 17px 8px; line-height: 50px; @@ -663,9 +658,6 @@ table tr.summary td { .summary .info { margin-left: 40px; } -.has-favorites .summary .info { - margin-left: 90px; -} table.dragshadow { width:auto; diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index c636544e6c..2c97bf737b 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1207,18 +1207,17 @@ td.append( '' ); - } else { - td.append('
'); } var linkElem = $('').attr({ "class": "name", "href": linkUrl }); + linkElem.append('
'); + // from here work on the display name name = fileData.displayName || name; diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index e6724e371c..caa7b731f7 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -175,7 +175,7 @@ } } var $icon = $(renderStar(isFavorite)); - $tr.find('td:first').prepend($icon); + $tr.find('td.filename .thumbnail').append($icon); return $tr; }; var oldElementToFile = fileList.elementToFile; diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 836a5e5ce7..8530e330d4 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -750,7 +750,7 @@ describe('OCA.Files.FileList tests', function() { doRename(); expect(fileList.findFileEl('Tu_after_three.txt').find('.thumbnail').parent().attr('class')) - .toEqual('icon-loading-small'); + .toContain('icon-loading-small'); deferredRename.reject(409); @@ -838,7 +838,7 @@ describe('OCA.Files.FileList tests', function() { fileList.move('One.txt', '/somedir'); expect(fileList.findFileEl('One.txt').find('.thumbnail').parent().attr('class')) - .toEqual('icon-loading-small'); + .toContain('icon-loading-small'); expect(moveStub.calledOnce).toEqual(true); @@ -935,7 +935,7 @@ describe('OCA.Files.FileList tests', function() { fileList.copy('One.txt', '/somedir'); expect(fileList.findFileEl('One.txt').find('.thumbnail').parent().attr('class')) - .toEqual('icon-loading-small'); + .toContain('icon-loading-small'); expect(copyStub.calledOnce).toEqual(true); @@ -3150,7 +3150,7 @@ describe('OCA.Files.FileList tests', function() { fileList.showFileBusyState('Two.jpg', true); expect($tr.hasClass('busy')).toEqual(true); expect($tr.find('.thumbnail').parent().attr('class')) - .toEqual('icon-loading-small'); + .toContain('icon-loading-small'); fileList.showFileBusyState('Two.jpg', false); diff --git a/core/search/css/results.css b/core/search/css/results.css index 2e3791a47f..5369a3093d 100644 --- a/core/search/css/results.css +++ b/core/search/css/results.css @@ -30,9 +30,6 @@ padding: 28px 0 28px 56px; font-size: 18px; } -.has-favorites:not(.hidden) ~ #searchresults .status { - padding-left: 102px; -} #searchresults .status.fixed { position: fixed; bottom: 0; @@ -67,10 +64,6 @@ background-position: right center; background-repeat: no-repeat; } -.has-favorites:not(.hidden) ~ #searchresults td.icon { - width: 86px; - background-size: 32px; -} #searchresults tr.template { display: none; From 6b8713e8b6f45f79ec7cffd150c7f0136d7fb5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 Sep 2017 16:03:00 +0200 Subject: [PATCH 012/105] Remove "has-favorites" class from file list table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "has-favorites" CSS class is no longer used after moving the favorite mark to the top right corner of the thumbnail, so there is no need to add it to the table. Signed-off-by: Daniel Calviño Sánchez --- apps/files/js/tagsplugin.js | 1 - apps/files/tests/js/tagspluginspec.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index caa7b731f7..2286477750 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -162,7 +162,6 @@ _extendFileList: function(fileList) { // extend row prototype - fileList.$el.addClass('has-favorites'); var oldCreateRow = fileList._createRow; fileList._createRow = function(fileData) { var $tr = oldCreateRow.apply(this, arguments); diff --git a/apps/files/tests/js/tagspluginspec.js b/apps/files/tests/js/tagspluginspec.js index b76b3c02dd..363a8bb0e1 100644 --- a/apps/files/tests/js/tagspluginspec.js +++ b/apps/files/tests/js/tagspluginspec.js @@ -71,9 +71,6 @@ describe('OCA.Files.TagsPlugin tests', function() { expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', OC.TAG_FAVORITE]); expect($tr.attr('data-favorite')).toEqual('true'); }); - it('adds has-favorites class on table', function() { - expect(fileList.$el.hasClass('has-favorites')).toEqual(true); - }); }); describe('Applying tags', function() { it('through FileActionsMenu sends request to server and updates icon', function() { From f392e78d5b525abbb04057b91c0adfba212c00c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 29 Sep 2017 01:36:10 +0200 Subject: [PATCH 013/105] Move checkboxes to their own column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The selection column is not only a visual column, but also a real column of the file list table. Unlike other columns whose width is reduced in space constrained screens the selection column must stay the same so the tapping area is large enough to be easily usable The selection column does not appear in the search results table, so its contents have to be explicitly aligned with those of the main table based on whether the main table has a selection column or not (using the "has-selection" CSS class in the same way as the "has-favorite" CSS class was being used when there was a column for favorite actions). In the tests the ":visible" selector can no longer be used. That selector matches elements with a width or height that is greater than zero, but the dimensions calculated in the unit tests are not reliable; the width of the link was zero before these changes, and now moving the checkbox to its own column causes the height of the link to become zero too, so it no longer matches the ":visible" selector even if it is not hidden. As hidding and showing the link is based on its "display" CSS property its value is the one checked now. Signed-off-by: Daniel Calviño Sánchez --- apps/files/css/files.scss | 41 +++++++---------------- apps/files/css/mobile.scss | 4 --- apps/files/js/filelist.js | 44 ++++++++++++++++++------- apps/files/templates/list.php | 10 +++--- apps/files/tests/js/filelistSpec.js | 22 ++++++++----- apps/files_trashbin/templates/index.php | 10 +++--- core/search/css/results.css | 9 ++++- 7 files changed, 77 insertions(+), 63 deletions(-) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 80a2b44ac1..65519ae2e1 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -438,46 +438,27 @@ table td.filename .uploadtext { opacity: .5; } +table td.selection { + padding: 0; +} + /* File checkboxes */ -#fileList tr td.filename>.selectCheckBox + label:before { +#fileList tr td.selection>.selectCheckBox + label:before { opacity: 0.3; - position: absolute; - bottom: 4px; - right: 0; - z-index: 10; } /* Show checkbox with full opacity when hovering, checked, or selected */ -#fileList tr:hover td.filename>.selectCheckBox + label:before, -#fileList tr:focus td.filename>.selectCheckBox + label:before, -#fileList tr td.filename>.selectCheckBox:checked + label:before, -#fileList tr.selected td.filename>.selectCheckBox + label:before { +#fileList tr:hover td.selection>.selectCheckBox + label:before, +#fileList tr:focus td.selection>.selectCheckBox + label:before, +#fileList tr td.selection>.selectCheckBox:checked + label:before, +#fileList tr.selected td.selection>.selectCheckBox + label:before { opacity: 1; } /* Use label to have bigger clickable size for checkbox */ -#fileList tr td.filename>.selectCheckBox + label, +#fileList tr td.selection>.selectCheckBox + label, .select-all + label { - background-position: 30px 30px; - height: 50px; - position: absolute; - width: 50px; - z-index: 5; -} -#fileList tr td.filename>.selectCheckBox { - /* sometimes checkbox height is bigger (KDE/Qt), so setting to absolute - * to prevent it to increase the height */ - position: absolute; - z-index: 10; -} -.select-all + label { - top: 0; -} -.select-all + label:before { - position: absolute; - top: 18px; - left: 18px; - z-index: 10; + padding: 16px; } #fileList tr td.filename { diff --git a/apps/files/css/mobile.scss b/apps/files/css/mobile.scss index eefc92c816..e7b75910fa 100644 --- a/apps/files/css/mobile.scss +++ b/apps/files/css/mobile.scss @@ -24,10 +24,6 @@ table td.date { table td { padding: 0; } -/* and accordingly fix left margin of file list summary on mobile */ -.summary .info { - margin-left: 105px; -} /* remove shift for multiselect bar to account for missing navigation */ table.multiselect thead { diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 2c97bf737b..0d45c29b25 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -332,7 +332,7 @@ this.$fileList.on('click','td.filename>a.name, td.filesize, td.date', _.bind(this._onClickFile, this)); - this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this)); + this.$fileList.on('change', 'td.selection>.selectCheckBox', _.bind(this._onClickFileCheckbox, this)); this.$el.on('show', _.bind(this._onShow, this)); this.$el.on('urlChanged', _.bind(this._onUrlChanged, this)); this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this)); @@ -593,7 +593,7 @@ * @param {bool} state true to select, false to deselect */ _selectFileEl: function($tr, state, showDetailsView) { - var $checkbox = $tr.find('td.filename>.selectCheckBox'); + var $checkbox = $tr.find('td.selection>.selectCheckBox'); var oldData = !!this._selectedFiles[$tr.data('id')]; var data; $checkbox.prop('checked', state); @@ -649,7 +649,7 @@ else { this._lastChecked = $tr; } - var $checkbox = $tr.find('td.filename>.selectCheckBox'); + var $checkbox = $tr.find('td.selection>.selectCheckBox'); this._selectFileEl($tr, !$checkbox.prop('checked')); this.updateSelectionSummary(); } else { @@ -704,7 +704,7 @@ */ _onClickSelectAll: function(e) { var checked = $(e.target).prop('checked'); - this.$fileList.find('td.filename>.selectCheckBox').prop('checked', checked) + this.$fileList.find('td.selection>.selectCheckBox').prop('checked', checked) .closest('tr').toggleClass('selected', checked); this._selectedFiles = {}; this._selectionSummary.clear(); @@ -1063,6 +1063,13 @@ this.$fileList.empty(); + if (this._allowSelection) { + // The results table, which has no selection column, checks + // whether the main table has a selection column or not in order + // to align its contents with those of the main table. + this.$el.addClass('has-selection'); + } + // clear "Select all" checkbox this.$el.find('.select-all').prop('checked', false); @@ -1192,6 +1199,20 @@ path = this.getCurrentDirectory(); } + // selection td + if (this._allowSelection) { + td = $(''); + + td.append( + '' + ); + + tr.append(td); + } + // filename td td = $(''); @@ -1203,14 +1224,6 @@ else { linkUrl = this.getDownloadUrl(name, path, type === 'dir'); } - if (this._allowSelection) { - td.append( - '' - ); - } var linkElem = $('').attr({ "class": "name", "href": linkUrl @@ -2613,6 +2626,13 @@ */ _createSummary: function() { var $tr = $(''); + + if (this._allowSelection) { + // Dummy column for selection, as all rows must have the same + // number of columns. + $tr.append(''); + } + this.$el.find('tfoot').append($tr); return new OCA.Files.FileSummary($tr, {config: this._filesConfig}); diff --git a/apps/files/templates/list.php b/apps/files/templates/list.php index 46bd9351e3..483bdcd2e3 100644 --- a/apps/files/templates/list.php +++ b/apps/files/templates/list.php @@ -41,12 +41,14 @@ +