diff --git a/.gitignore b/.gitignore index c7b6b3966355b..a11e3a14597cf 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ nbproject /build/jsdocs/ /npm-debug.log /PhantomJS_* +/build/package-lock.json # puphpet puphpet diff --git a/apps/files/css/detailsView.scss b/apps/files/css/detailsView.scss index e0c1bbfa09510..f64a370285037 100644 --- a/apps/files/css/detailsView.scss +++ b/apps/files/css/detailsView.scss @@ -7,12 +7,6 @@ clear: both; } -#app-sidebar .mainFileInfoView { - margin-right: 20px; /* accommodate for close icon */ - float:left; - display:block; - width: 100%; -} #app-sidebar .mainFileInfoView .icon { display: inline-block; diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 2a71af038cf83..017253fdf8e83 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -94,7 +94,8 @@ @include icon-color('star-dark', 'files', $color-black, 2, true); } .nav-icon-sharingin, -.nav-icon-sharingout { +.nav-icon-sharingout, +.nav-icon-shareoverview { @include icon-color('share', 'files', $color-black); } .nav-icon-sharinglinks { diff --git a/apps/files_sharing/css/public.scss b/apps/files_sharing/css/public.scss index 2e788a06c40f1..583912ad23614 100644 --- a/apps/files_sharing/css/public.scss +++ b/apps/files_sharing/css/public.scss @@ -169,3 +169,8 @@ thead { opacity: .57; margin-top: 10px; } + +#note { + text-align: center; + padding: 10px; +} diff --git a/apps/files_sharing/css/sharetabview.scss b/apps/files_sharing/css/sharetabview.scss index b4b64daff2b0b..83790c9ec4fec 100644 --- a/apps/files_sharing/css/sharetabview.scss +++ b/apps/files_sharing/css/sharetabview.scss @@ -2,143 +2,242 @@ min-height: 100px; } -.shareTabView .oneline { - white-space: nowrap; - position: relative; -} - -.shareTabView .shareWithLoading { - padding-left: 10px; - right: 35px; - top: 0px; -} - -.shareTabView .shareWithConfirm, -.shareTabView .clipboardButton, -.shareTabView .linkPass .icon-loading-small { - position: absolute; - right: -7px; - top: -2px; - padding: 14px; -} - -.shareTabView .shareWithConfirm { - opacity: .5; -} - -.shareTabView .shareWithField:focus ~ .shareWithConfirm { - opacity: 1; -} - -.shareTabView .linkMore { - position: absolute; - right: -7px; - top: -4px; - padding: 14px; -} - -/* fix the popup menu because the button is shifted and then the menu is not aligned */ -.shareTabView .popovermenu.socialSharingMenu { - right: -7px; -} - -.shareTabView .popovermenu .clipboardButton { - position: relative; - top: initial; - right: initial; - padding: 0; -} - -.shareTabView label { - white-space: nowrap; -} - -.shareTabView input[type="checkbox"] { - margin: 0 3px 0 8px; - vertical-align: middle; -} - -.shareTabView input[type="text"].shareWithField, -.shareTabView input[type="text"].emailField, -.shareTabView input[type="text"].linkText, -.shareTabView input[type="password"] { - width: 100%; - box-sizing: border-box; - padding-right: 32px; - text-overflow: ellipsis; -} - -.shareTabView form { - font-size: 100%; - margin-left: 0; - margin-right: 0; -} - -#shareWithList { +.share-autocomplete-item { + display: flex; + .autocomplete-item-text { + margin-left: 10px; + margin-right: 10px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + line-height: 32px; + vertical-align: middle; + } +} + +.shareTabView { + .oneline { + white-space: nowrap; + position: relative; + } + .shareWithLoading { + padding-left: 10px; + right: 35px; + top: 0px; + } + .shareWithConfirm, + .clipboardButton, + .linkPass .icon-loading-small { + position: absolute; + right: 2px; + top: 6px; + padding: 14px; + } + .shareWithConfirm { + opacity: 0.5; + } + .shareWithField:focus ~ .shareWithConfirm { + opacity: 1; + } + .linkMore { + position: absolute; + right: -7px; + top: -4px; + padding: 14px; + } + .popovermenu { + .datepicker { + margin-left: 35px; + } + .clipboardButton { + position: relative; + top: initial; + right: initial; + padding: 0; + } + .share-add { + input.share-note-delete { + display: none; + border: none; + background-color: transparent; + width: 44px !important; + padding: 0; + flex: 0 0 44px; + margin-left: auto; + } + } + // note + .share-note-form { + span.icon-note { + position: relative; + } + textarea.share-note { + margin: 0; + width: 200px; + min-height: 70px; + resize: none; + + input.share-note-submit { + position: absolute; + width: 44px !important; + height: 44px; + bottom: 0px; + right: 10px; + margin: 0; + background-color: transparent; + border: none; + opacity: .7; + &:hover, + &:focus, + &:active { + opacity: 1; + } + } + } + // fix for popover link share + &.share-note-link { + margin-bottom: 10px; + } + } + } + .linkPass .icon-loading-small { + margin-right: 0px; + } + .icon { + background-size: 16px 16px; + } + .shareWithList .icon-loading-small:not(.hidden) + span, + .linkShareView .icon-loading-small:not(.hidden) + input + label:before { + /* Hide if loader is visible */ + display: none !important; + } + input { + &[type='checkbox'] { + margin: 0 3px 0 8px; + vertical-align: middle; + } + &[type='text'] { + &.shareWithField, + &.emailField { + width: 100%; + box-sizing: border-box; + padding-right: 32px; + text-overflow: ellipsis; + } + } + &[type='text'].linkText + &[type='password'].linkPassText, + &[type='password'].passwordField { + width: 180px !important; + } + } + form { + font-size: 100%; + margin-left: 0; + margin-right: 0; + } + // share note on the sidebar + .share-note { + border-radius: var(--border-radius); + margin-bottom: 10px; + margin-left: 37px; + } +} + +// Sharing tab users list +.shareWithList { list-style-type: none; - padding: 0 0 16px; -} - -#shareWithList > li { - padding-top: 5px; - padding-bottom: 5px; - white-space: normal; + display: flex; + flex-direction: column; + > li { + height: 44px; + white-space: normal; + display: inline-flex; + align-items: center; + position: relative; + .avatar { + width: 32px; + height: 32px; + background-color: var(--color-background-darker); + } + } + .unshare img { + vertical-align: text-bottom; + /* properly align icons */ + } + .sharingOptionsGroup { + margin-left: auto; + display: flex; + align-items: center; + // can edit label + > .shareOption > label { + padding: 13px; + padding-right: 0; + } + // more menu + > .share-menu { + position: relative; + display: block; + .icon-more { + padding: 14px; + height: 16px; + width: 16px; + opacity: .5; + display: block; + cursor: pointer; + } + &:hover, + &:focus, + &:active { + .icon-more { + opacity: .7;; + } + } + } + } + .username { + padding: 0 8px; + } +} + +.ui-autocomplete { + /* limit dropdown height to 4 1/2 entries */ + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; + z-index: 1550 !important; +} + +.notCreatable { + padding-left: 12px; + padding-top: 12px; + color: var(--color-text-lighter); +} + +.contactsmenu-popover { + left: -6px; + right: auto; + padding: 3px 6px; + top: 100%; + margin-top: 0; + li.hidden { + display: none !important; + } + &:after { + left: 8px; + right: auto; + } +} + +.reshare, +#link label, +#expiration label { display: inline-flex; align-items: center; + .avatar { + margin-right: 5px; + } } -#shareWithList .unshare img { - vertical-align: text-bottom; /* properly align icons */ -} - -#shareWithList .sharingOptionsGroup > a .icon { - padding: 7px; - vertical-align: middle; - opacity: .5; -} - -#shareWithList .sharingOptionsGroup .popovermenu:after { - right: 3px; -} - -#shareWithList label input[type=checkbox] { - margin-left: 0; +.resharerInfoView.subView { position: relative; -} -#shareWithList .username { - padding-right: 8px; - white-space: nowrap; - text-overflow: ellipsis; - display: inline-block; - overflow: hidden; - vertical-align: middle; -} -#shareWithList li .sharingOptionsGroup > .shareOption > label { - padding: 6px; - margin-right: 8px; - vertical-align: text-top; -} - -.shareTabView .icon-loading-small { - display: inline-block; - z-index: 1; - vertical-align: text-top; -} - -.shareTabView .shareWithList .icon-loading-small:not(.hidden) + span, -.shareTabView .linkShareView .icon-loading-small:not(.hidden) + input + label:before { - /* Hide if loader is visible */ - display: none !important; -} - -.linkShareView { - margin-top: 16px; -} - -.shareTabView .linkPass .icon-loading-small { - margin-right: 0px; -} - -.shareTabView .icon { - background-size: 16px 16px; -} +} \ No newline at end of file diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index d30d5a05a2204..33782d21b5f0d 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -144,6 +144,7 @@ protected function formatShare(\OCP\Share\IShare $share, Node $recipientNode = n 'expiration' => null, 'token' => null, 'uid_file_owner' => $share->getShareOwner(), + 'note' => $share->getNote(), 'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(), ]; @@ -698,17 +699,21 @@ public function getShares( * @param string $password * @param string $publicUpload * @param string $expireDate + * @param string $note * @return DataResponse - * @throws OCSNotFoundException + * @throws LockedException + * @throws NotFoundException * @throws OCSBadRequestException * @throws OCSForbiddenException + * @throws OCSNotFoundException */ public function updateShare( string $id, int $permissions = null, string $password = null, string $publicUpload = null, - string $expireDate = null + string $expireDate = null, + string $note = null ): DataResponse { try { $share = $this->getShareById($id); @@ -722,10 +727,14 @@ public function updateShare( throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); } - if ($permissions === null && $password === null && $publicUpload === null && $expireDate === null) { + if ($permissions === null && $password === null && $publicUpload === null && $expireDate === null && $note === null) { throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given')); } + if($note !== null) { + $share->setNote($note); + } + /* * expirationdate, password and publicUpload only make sense for link shares */ diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index 0b30a599c7ffa..bd1331a090884 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -262,6 +262,7 @@ public function showShare($path = ''): TemplateResponse { $shareTmpl['owner'] = $share->getShareOwner(); $shareTmpl['filename'] = $share->getNode()->getName(); $shareTmpl['directory_path'] = $share->getTarget(); + $shareTmpl['note'] = $share->getNote(); $shareTmpl['mimetype'] = $share->getNode()->getMimetype(); $shareTmpl['previewSupported'] = $this->previewManager->isMimeSupported($share->getNode()->getMimetype()); $shareTmpl['dirToken'] = $this->getToken(); diff --git a/apps/files_sharing/templates/public.php b/apps/files_sharing/templates/public.php index 476f0851547f6..81729c179fcf6 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -29,6 +29,12 @@ + +
+ t('Note:')); p(' ' . $_['note']); ?> +
+ +
diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 5d376f2d4f795..30041c3a27b99 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -238,7 +238,7 @@ public function testGetGetShareNotExists() { */ public function createShare($id, $shareType, $sharedWith, $sharedBy, $shareOwner, $path, $permissions, - $shareTime, $expiration, $parent, $target, $mail_send, $token=null, + $shareTime, $expiration, $parent, $target, $mail_send, $note = '', $token=null, $password=null) { $share = $this->getMockBuilder(IShare::class)->getMock(); $share->method('getId')->willReturn($id); @@ -248,6 +248,7 @@ public function createShare($id, $shareType, $sharedWith, $sharedBy, $shareOwner $share->method('getShareOwner')->willReturn($shareOwner); $share->method('getNode')->willReturn($path); $share->method('getPermissions')->willReturn($permissions); + $share->method('getNote')->willReturn($note); $time = new \DateTime(); $time->setTimestamp($shareTime); $share->method('getShareTime')->willReturn($time); @@ -310,7 +311,8 @@ public function dataGetShare() { null, 6, 'target', - 0 + 0, + 'personal note' ); $expected = [ 'id' => 100, @@ -334,6 +336,7 @@ public function dataGetShare() { 'storage' => 101, 'mail_send' => 0, 'uid_file_owner' => 'ownerId', + 'note' => 'personal note', 'displayname_file_owner' => 'ownerDisplay', 'mimetype' => 'myMimeType', ]; @@ -352,7 +355,8 @@ public function dataGetShare() { null, 6, 'target', - 0 + 0, + 'personal note' ); $expected = [ 'id' => 101, @@ -376,6 +380,7 @@ public function dataGetShare() { 'storage' => 101, 'mail_send' => 0, 'uid_file_owner' => 'ownerId', + 'note' => 'personal note', 'displayname_file_owner' => 'ownerDisplay', 'mimetype' => 'myFolderMimeType', ]; @@ -396,6 +401,7 @@ public function dataGetShare() { 6, 'target', 0, + 'personal note', 'token', 'password' ); @@ -422,6 +428,7 @@ public function dataGetShare() { 'mail_send' => 0, 'url' => 'url', 'uid_file_owner' => 'ownerId', + 'note' => 'personal note', 'displayname_file_owner' => 'ownerDisplay', 'mimetype' => 'myFolderMimeType', ]; @@ -455,7 +462,7 @@ public function testGetShare(\OCP\Share\IShare $share, array $result) { ->willReturn(true); $this->shareManager - ->expects($this->once()) + ->expects($this->any()) ->method('getShareById') ->with($share->getFullId(), 'currentUser') ->willReturn($share); @@ -501,6 +508,8 @@ public function testGetShare(\OCP\Share\IShare $share, array $result) { ['group', $group], ])); + $d = $ocs->getShare($share->getId())->getData()[0]; + $this->assertEquals($result, $ocs->getShare($share->getId())->getData()[0]); } @@ -1810,9 +1819,10 @@ public function dataFormatShare() { ->setNode($file) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); - /* User backend down */ + // User backend down $result[] = [ [ 'id' => 42, @@ -1836,12 +1846,12 @@ public function dataFormatShare() { 'file_target' => 'myTarget', 'share_with' => 'recipient', 'share_with_displayname' => 'recipient', + 'note' => 'personal note', 'mail_send' => 0, 'mimetype' => 'myMimeType', ], $share, [], false ]; - - /* User backend up */ + // User backend up $result[] = [ [ 'id' => 42, @@ -1855,6 +1865,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'ownerDN', + 'note' => 'personal note', 'path' => 'file', 'item_type' => 'file', 'storage_id' => 'storageId', @@ -1883,9 +1894,9 @@ public function dataFormatShare() { ->setNode($file) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); - - /* User backend down */ + // User backend down $result[] = [ [ 'id' => 42, @@ -1899,6 +1910,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => 'personal note', 'path' => 'file', 'item_type' => 'file', 'storage_id' => 'storageId', @@ -1915,6 +1927,7 @@ public function dataFormatShare() { ]; // with existing group + $share = \OC::$server->getShareManager()->newShare(); $share->setShareType(\OCP\Share::SHARE_TYPE_GROUP) ->setSharedWith('recipientGroup') @@ -1924,6 +1937,7 @@ public function dataFormatShare() { ->setNode($file) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -1939,6 +1953,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => 'personal note', 'path' => 'file', 'item_type' => 'file', 'storage_id' => 'storageId', @@ -1964,6 +1979,7 @@ public function dataFormatShare() { ->setNode($file) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ [ @@ -1978,6 +1994,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => 'personal note', 'path' => 'file', 'item_type' => 'file', 'storage_id' => 'storageId', @@ -2004,6 +2021,7 @@ public function dataFormatShare() { ->setPassword('mypassword') ->setExpirationDate(new \DateTime('2001-01-02T00:00:00')) ->setToken('myToken') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -2019,6 +2037,7 @@ public function dataFormatShare() { 'token' => 'myToken', 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => 'personal note', 'path' => 'file', 'item_type' => 'file', 'storage_id' => 'storageId', @@ -2044,6 +2063,7 @@ public function dataFormatShare() { ->setNode($folder) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -2059,6 +2079,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => 'personal note', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2101,6 +2122,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2142,6 +2164,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2183,6 +2206,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2207,6 +2231,7 @@ public function dataFormatShare() { ->setPermissions(\OCP\Constants::PERMISSION_READ) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -2238,6 +2263,7 @@ public function dataFormatShare() { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php index fb417878647b0..a01560d0288a8 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -192,6 +192,9 @@ public function testShowShareNotAuthenticated() { public function testShowShare() { + + $note = 'personal note'; + $this->shareController->setToken('token'); $owner = $this->getMockBuilder(IUser::class)->getMock(); @@ -210,6 +213,7 @@ public function testShowShare() { $share->setPassword('password') ->setShareOwner('ownerUID') ->setNode($file) + ->setNote($note) ->setTarget('/file1.txt'); $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); @@ -283,6 +287,7 @@ public function testShowShare() { 'shareUrl' => null, 'previewImage' => null, 'previewURL' => 'downloadURL', + 'note' => $note ); $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index 1a1855b9c4493..73e962e3292be 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -506,6 +506,61 @@ protected function sendPassword(IShare $share, $password) { return true; } + protected function sendNote(IShare $share) { + + $recipient = $share->getSharedWith(); + + + $filename = $share->getNode()->getName(); + $initiator = $share->getSharedBy(); + $note = $share->getNote(); + + $initiatorUser = $this->userManager->get($initiator); + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; + $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; + + $plainHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add:', [$initiatorDisplayName, $filename]); + $htmlHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add', [$initiatorDisplayName, $filename]); + + $message = $this->mailer->createMessage(); + + $emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote'); + + $emailTemplate->setSubject($this->l->t('»%s« added a note to a file shared with you', [$initiatorDisplayName])); + $emailTemplate->addHeader(); + $emailTemplate->addHeading(htmlspecialchars($htmlHeading), $plainHeading); + $emailTemplate->addBodyText(htmlspecialchars($note), $note); + + $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', + ['token' => $share->getToken()]); + $emailTemplate->addBodyButton( + $this->l->t('Open »%s«', [$filename]), + $link + ); + + // The "From" contains the sharers name + $instanceName = $this->defaults->getName(); + $senderName = $this->l->t( + '%1$s via %2$s', + [ + $initiatorDisplayName, + $instanceName + ] + ); + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); + if ($initiatorEmailAddress !== null) { + $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); + $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); + } else { + $emailTemplate->addFooter(); + } + + $message->setTo([$recipient]); + $message->useTemplate($emailTemplate); + $this->mailer->send($message); + + } + /** * send auto generated password to the owner. This happens if the admin enforces * a password for mail shares and forbid to send the password by mail to the recipient @@ -662,8 +717,13 @@ public function update(IShare $share, $plainTextPassword = null) { ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('password', $qb->createNamedParameter($share->getPassword())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('note', $qb->createNamedParameter($share->getNote())) ->execute(); + if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') { + $this->sendNote($share); + } + return $share; } @@ -904,6 +964,7 @@ protected function createShareObject($data) { ->setPermissions((int)$data['permissions']) ->setTarget($data['file_target']) ->setMailSend((bool)$data['mail_send']) + ->setNote($data['note']) ->setToken($data['token']); $shareTime = new \DateTime(); diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php index 95d746cfb46a2..f0d99e6026cda 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -342,15 +342,17 @@ public function testUpdate() { $uidOwner = 'user2'; $permissions = 1; $token = 'token'; + $note = 'personal note'; $instance = $this->getInstance(); - $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $note); $this->share->expects($this->once())->method('getPermissions')->willReturn($permissions + 1); $this->share->expects($this->once())->method('getShareOwner')->willReturn($uidOwner); $this->share->expects($this->once())->method('getSharedBy')->willReturn($sharedBy); + $this->share->expects($this->any())->method('getNote')->willReturn($note); $this->share->expects($this->atLeastOnce())->method('getId')->willReturn($id); $this->assertSame($this->share, @@ -372,6 +374,7 @@ public function testUpdate() { $this->assertSame($uidOwner, $result[0]['uid_owner']); $this->assertSame($permissions + 1, (int)$result[0]['permissions']); $this->assertSame($token, $result[0]['token']); + $this->assertSame($note, $result[0]['note']); } public function testDelete() { @@ -478,7 +481,7 @@ public function testGetShareByToken() { $instance = $this->getInstance(['createShareObject']); $idMail = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); - $idPublic = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, \OCP\Share::SHARE_TYPE_LINK); + $idPublic = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, '', \OCP\Share::SHARE_TYPE_LINK); $this->assertTrue($idMail !== $idPublic); @@ -490,9 +493,9 @@ function ($data) use ($idMail) { } ); - $this->assertInstanceOf('OCP\Share\IShare', - $instance->getShareByToken('token') - ); + $result = $instance->getShareByToken('token'); + + $this->assertInstanceOf('OCP\Share\IShare', $result); } /** @@ -511,7 +514,7 @@ public function testGetShareByTokenFailed() { $instance = $this->getInstance(['createShareObject']); $idMail = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); - $idPublic = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, "token2", \OCP\Share::SHARE_TYPE_LINK); + $idPublic = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, "token2", '', \OCP\Share::SHARE_TYPE_LINK); $this->assertTrue($idMail !== $idPublic); @@ -631,7 +634,7 @@ public function testGetRawShareFailed() { $this->invokePrivate($instance, 'getRawShare', [$id+1]); } - private function createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $shareType = \OCP\Share::SHARE_TYPE_EMAIL) { + private function createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $note='', $shareType = \OCP\Share::SHARE_TYPE_EMAIL) { $qb = $this->connection->getQueryBuilder(); $qb->insert('share') ->setValue('share_type', $qb->createNamedParameter($shareType)) @@ -643,6 +646,7 @@ private function createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) ->setValue('permissions', $qb->createNamedParameter($permissions)) ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('note', $qb->createNamedParameter($note)) ->setValue('stime', $qb->createNamedParameter(time())); /* diff --git a/core/Migrations/Version14000Date20180712153140.php b/core/Migrations/Version14000Date20180712153140.php new file mode 100644 index 0000000000000..268a479eaad78 --- /dev/null +++ b/core/Migrations/Version14000Date20180712153140.php @@ -0,0 +1,43 @@ + + * + * @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\Core\Migrations; + +use OCP\DB\ISchemaWrapper; +use OCP\Migration\SimpleMigrationStep; + +/** + * add column for share notes + * + * Class Version14000Date20180712153140 + */ +class Version14000Date20180712153140 extends SimpleMigrationStep { + public function changeSchema(\OCP\Migration\IOutput $output, \Closure $schemaClosure, array $options) { + + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('share'); + $table->addColumn('note', 'text', ['notnull' => false]); + + return $schema; + } +} diff --git a/core/css/apps.scss b/core/css/apps.scss index d524dd94bb77e..86f0e622405d3 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -74,6 +74,13 @@ kbd { /* Navigation: folder like structure */ #app-navigation { width: $navigation-width; + position: sticky; + top: $header-height; + left: 0; + z-index: 1500; + overflow-y: auto; + overflow-x: hidden; + height: calc(100vh - #{$header-height}); box-sizing: border-box; background-color: var(--color-main-background); -webkit-user-select: none; @@ -320,9 +327,6 @@ kbd { &.hidden { display: none; } - &.without-app-settings { - padding-bottom: 0; - } /** * Button styling for menu, edit and undo @@ -581,12 +585,7 @@ kbd { padding-top: $header-height; box-sizing: border-box; position: relative; - overflow-x: hidden; display: flex; - /* trick: scroll #app-content and not the body - * to avoid double scrollbar with sidebar - */ - max-height: 100vh; } /* APP-CONTENT AND WRAPPER ------------------------------------------ */ @@ -637,17 +636,19 @@ kbd { min-width: $sidebar-min-width; max-width: $sidebar-max-width; display: block; - position: relative; + position: sticky; + top: $header-height; + right:0; + overflow-y: auto; + overflow-x: hidden; + z-index: 1500; + height: calc(100vh - #{$header-height}); background: var(--color-main-background); border-left: 1px solid var(--color-border); - overflow-x: hidden; - overflow-y: auto; flex-shrink: 0; - transition: 300ms width ease-in-out, - 300ms min-width ease-in-out; + // no animations possible, use OC.Apps.showAppSidebar &.disappear { - width: 0; - min-width: 0; + display: none; } } @@ -880,6 +881,11 @@ $popovericon-size: 16px; li { display: flex; flex: 0 0 auto; + + &.hidden { + display: none; + } + > button, > a, > .menuitem { @@ -895,6 +901,7 @@ $popovericon-size: 16px; box-shadow: none; width: 100%; color: var(--color-main-text); + white-space: nowrap; /* Override the app-navigation li opacity */ opacity: .7 !important; span[class^='icon-'], @@ -943,6 +950,7 @@ $popovericon-size: 16px; width: 150px; line-height: 1.6em; padding: 8px 0; + white-space: normal; } > select { margin: 0; diff --git a/core/css/ie.scss b/core/css/ie.scss new file mode 100644 index 0000000000000..ec7f51065e426 --- /dev/null +++ b/core/css/ie.scss @@ -0,0 +1,11 @@ + +#app-navigation, +#app-sidebar { + position: fixed !important; +} +#app-content { + width: $navigation-width !important; +} +#app-sidebar.disappear { + right: -$sidebar-max-width !important; +} \ No newline at end of file diff --git a/core/css/share.scss b/core/css/share.scss deleted file mode 100644 index 07489cd55a364..0000000000000 --- a/core/css/share.scss +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @copyright Copyright (c) 2016, John Molakvoæ - * @copyright Copyright (c) 2016, Morris Jobke - * @copyright Copyright (c) 2016, Julia Bode - * @copyright Copyright (c) 2016, Christoph Wurst - * @copyright Copyright (c) 2015, Hendrik Leppelsack - * @copyright Copyright (c) 2015, Jan-Christoph Borchardt - * @copyright Copyright (c) 2015, Vincent Petry - * @copyright Copyright (c) 2015, Arthur Schiwon - * @copyright Copyright (c) 2015, Roeland Jago Douma - * @copyright Copyright (c) 2015, Morris Jobke - * - * @license GNU AGPL version 3 or any later version - * - */ - -/* SHARE TAB STYLING -------------------------------------------------------- */ -.shareTabView { - .unshare.icon-loading-small { - margin-top: 1px; - } - .shareWithLoading, .linkShare .icon-loading-small { - display: inline-block !important; - padding-left: 10px; - } - .shareWithLoading { - position: relative; - right: 70px; - top: 2px; - } - .icon-loading-small.hidden { - display: none !important; - } - .avatar { - margin-right: 8px; - display: inline-block; - overflow: hidden; - vertical-align: middle; - width: 32px; - height: 32px; - } - label { - font-weight: 400; - white-space: nowrap; - } - input[type='radio'].radio + label { - margin-left: -1px; - } - input[type='checkbox'] { - margin: 0 3px 0 8px; - vertical-align: middle; - } - input[type='submit'] { - margin-left: 7px; - } - form { - font-size: 100%; - margin-left: 0; - margin-right: 0; - } - .error { - color: var(--color-error); - border-color: var(--color-error); - } - .mailView .icon-mail { - opacity: 0.5; - } -} - -.share-autocomplete-item { - display: flex; - .autocomplete-item-text { - margin-left: 10px; - margin-right: 10px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - line-height: 32px; - vertical-align: middle; - } -} - -.ui-autocomplete .autocomplete-note { - padding: 5px 10px; - color: rgba(0, 0, 0, .3); -} - -#shareWithList { - list-style-type: none; - padding: 8px; - > li { - position: relative; - padding-top: 10px; - padding-bottom: 10px; - font-weight: bold; - line-height: 21px; - white-space: normal; - width: 100%; - } - .sharingOptionsGroup { - flex-shrink: 0; - position: relative; - .popovermenu { - right: -11px; - top: 35px; - } - } - - .shareOption { - white-space: nowrap; - display: inline-block; - } - .unshare img { - vertical-align: text-bottom; - /* properly align icons */ - } - label input[type=checkbox] { - margin-left: 0; - position: relative; - } - .username { - padding-right: 8px; - white-space: nowrap; - text-overflow: ellipsis; - display: inline-block; - overflow: hidden; - vertical-align: middle; - flex-grow: 5; - } -} - -#link { - border-top: 1px solid var(--color-border); - padding-top: 8px; - #showPassword img { - padding-left: 5px; - width: 12px; - } -} - -.reshare, -#link label, -#expiration label { - display: inline-block; - padding: 6px 4px; -} - -.resharerInfoView.subView { - position: relative; -} - -#defaultExpireMessage, .reshare { - /* fix shared by text going out of box */ - white-space: normal; -} - -#defaultExpireMessage { - /* show message on new line */ - display: block; - padding-left: 4px; - /* TODO: style the dropdown in a proper way - border-box, etc. */ - width: 90%; -} - -.ui-autocomplete { - /* limit dropdown height to 4 1/2 entries */ - max-height: 200px; - overflow-y: auto; - overflow-x: hidden; -} - -.notCreatable { - padding-left: 12px; - padding-top: 12px; - color: var(--color-text-lighter); -} - -.contactsmenu-popover { - left: -6px; - right: auto; - padding: 3px 6px; - top: 100%; - margin-top: 0; - li.hidden { - display: none !important; - } - &:after { - left: 8px; - right: auto; - } -} - -.popovermenu .datepicker { - margin-left: 35px; -} - -.popovermenu .passwordField { - margin-left: 35px; - width: inherit !important; -} - -.ui-datepicker { - z-index: 1111 !important; -} diff --git a/core/css/styles.scss b/core/css/styles.scss index 30aa25d183cb2..9652b02e9d2ad 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -530,6 +530,7 @@ code { width: auto; border-radius: var(--border-radius); border: none; + z-index: 500 !important; .ui-state-default, .ui-widget-content .ui-state-default, diff --git a/core/img/actions/public-white.svg b/core/img/actions/public-white.svg new file mode 100644 index 0000000000000..d85defb6a0999 --- /dev/null +++ b/core/img/actions/public-white.svg @@ -0,0 +1 @@ + diff --git a/core/js/apps.js b/core/js/apps.js index b40883e88cf2c..473fec313a10c 100644 --- a/core/js/apps.js +++ b/core/js/apps.js @@ -27,8 +27,9 @@ */ exports.Apps.showAppSidebar = function($el) { var $appSidebar = $el || $('#app-sidebar'); - $appSidebar.removeClass('disappear'); - $('#content').addClass('with-app-sidebar').trigger(new $.Event('appresized')); + $appSidebar.removeClass('disappear') + .show('slide', { direction: 'right' }, 300); + $('#app-content').trigger(new $.Event('appresized')); }; /** @@ -39,8 +40,11 @@ */ exports.Apps.hideAppSidebar = function($el) { var $appSidebar = $el || $('#app-sidebar'); - $appSidebar.addClass('disappear'); - $('#content').removeClass('with-app-sidebar').trigger(new $.Event('appresized')); + $appSidebar.hide('slide', { direction: 'right' }, 300, + function() { + $appSidebar.addClass('disappear'); + }); + $('#app-content').trigger(new $.Event('appresized')); }; /** diff --git a/core/js/core.json b/core/js/core.json index 502e3a5797615..2ebc2e710ed0a 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -37,7 +37,6 @@ "shareconfigmodel.js", "shareitemmodel.js", "sharedialogview.js", - "sharedialogexpirationview.js", "sharedialoglinkshareview.js", "sharedialogresharerinfoview.js", "sharedialogshareelistview.js", diff --git a/core/js/merged-share-backend.json b/core/js/merged-share-backend.json index d39945b8f7933..63c3575a6660d 100644 --- a/core/js/merged-share-backend.json +++ b/core/js/merged-share-backend.json @@ -1,11 +1,10 @@ [ - "shareconfigmodel.js", - "shareitemmodel.js", - "sharesocialmanager.js", - "sharedialogresharerinfoview.js", - "sharedialoglinkshareview.js", - "sharedialogexpirationview.js", - "sharedialogshareelistview.js", - "sharedialogview.js", - "share.js" + "shareconfigmodel.js", + "shareitemmodel.js", + "sharesocialmanager.js", + "sharedialogresharerinfoview.js", + "sharedialoglinkshareview.js", + "sharedialogshareelistview.js", + "sharedialogview.js", + "share.js" ] diff --git a/core/js/sharedialogexpirationview.js b/core/js/sharedialogexpirationview.js deleted file mode 100644 index a9849ef916182..0000000000000 --- a/core/js/sharedialogexpirationview.js +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2015 - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -/* global moment, Handlebars */ - -(function() { - if (!OC.Share) { - OC.Share = {}; - } - - var TEMPLATE = - // currently expiration is only effective for link share. - // this is about to change in future. Therefore this is not included - // in the LinkShareView to ease reusing it in future. Then, - // modifications (getting rid of IDs) are still necessary. - '{{#if isLinkShare}}' + - '' + - '' + - '
' + - ' ' + - ' ' + - '
' + - ' {{#if isExpirationEnforced}}' + - // originally the expire message was shown when a default date was set, however it never had text - '{{defaultExpireMessage}}' + - ' {{/if}}' + - '{{/if}}' - ; - - /** - * @class OCA.Share.ShareDialogExpirationView - * @member {OC.Share.ShareItemModel} model - * @member {jQuery} $el - * @memberof OCA.Sharing - * @classdesc - * - * Represents the expiration part in the GUI of the share dialogue - * - */ - var ShareDialogExpirationView = OC.Backbone.View.extend({ - /** @type {string} **/ - id: 'shareDialogLinkShare', - - /** @type {OC.Share.ShareConfigModel} **/ - configModel: undefined, - - /** @type {Function} **/ - _template: undefined, - - /** @type {boolean} **/ - showLink: true, - - className: 'hidden', - - events: { - 'change .expirationCheckbox': '_onToggleExpiration', - 'change .datepicker': '_onChangeExpirationDate' - }, - - initialize: function(options) { - if(!_.isUndefined(options.configModel)) { - this.configModel = options.configModel; - } else { - throw 'missing OC.Share.ShareConfigModel'; - } - - var view = this; - this.configModel.on('change:isDefaultExpireDateEnforced', function() { - view.render(); - }); - - this.model.on('change:itemType', function() { - view.render(); - }); - - this.model.on('change:linkShare', function() { - view.render(); - }); - }, - - _onToggleExpiration: function(event) { - var $checkbox = $(event.target); - var state = $checkbox.prop('checked'); - // TODO: slide animation - this.$el.find('.expirationDateContainer').toggleClass('hidden', !state); - if (!state) { - // discard expiration date - this.model.get('linkShare').expiration = ''; - this.model.saveLinkShare({ - expireDate: '' - }); - } else { - this.$el.find('#expirationDate').focus(); - } - }, - - _onChangeExpirationDate: function(event) { - var $target = $(event.target); - $target.tooltip('hide'); - $target.removeClass('error'); - - var expiration = moment($target.val(), 'DD-MM-YYYY').format('YYYY-MM-DD'); - this.model.get('linkShare').expiration = expiration; - this.model.saveLinkShare({ - expiration: expiration - }, { - error: function(model, message) { - if (!message) { - $target.attr('title', t('core', 'Error setting expiration date')); - } else { - $target.attr('title', message); - } - $target.tooltip({gravity: 'n'}); - $target.tooltip('show'); - $target.addClass('error'); - } - }); - }, - - render: function() { - var defaultExpireMessage = ''; - var defaultExpireDays = this.configModel.get('defaultExpireDate'); - var isExpirationEnforced = this.configModel.get('isDefaultExpireDateEnforced'); - - if( (this.model.isFolder() || this.model.isFile()) - && isExpirationEnforced) { - defaultExpireMessage = t( - 'core', - 'The public link will expire no later than {days} days after it is created', - {'days': defaultExpireDays } - ); - } - - var isExpirationSet = !!this.model.get('linkShare').expiration || isExpirationEnforced; - - var expiration; - if (isExpirationSet) { - expiration = moment(this.model.get('linkShare').expiration, 'YYYY-MM-DD').format('DD-MM-YYYY'); - } - - this.$el.html(this.template({ - cid: this.cid, - setExpirationLabel: t('core', 'Set expiration date'), - expirationLabel: t('core', 'Expiration'), - expirationDatePlaceholder: t('core', 'Expiration date'), - defaultExpireMessage: defaultExpireMessage, - isLinkShare: this.model.get('linkShare').isLinkShare, - isExpirationSet: isExpirationSet, - isExpirationEnforced: isExpirationEnforced, - disableCheckbox: isExpirationEnforced && isExpirationSet, - expirationValue: expiration - })); - - // what if there is another date picker on that page? - var minDate = new Date(); - var maxDate = null; - // min date should always be the next day - minDate.setDate(minDate.getDate()+1); - - if(isExpirationSet) { - if(isExpirationEnforced) { - // TODO: hack: backend returns string instead of integer - var shareTime = this.model.get('linkShare').stime; - if (_.isNumber(shareTime)) { - shareTime = new Date(shareTime * 1000); - } - if (!shareTime) { - shareTime = new Date(); // now - } - shareTime = OC.Util.stripTime(shareTime).getTime(); - maxDate = new Date(shareTime + defaultExpireDays * 24 * 3600 * 1000); - } - } - $.datepicker.setDefaults({ - minDate: minDate, - maxDate: maxDate - }); - - this.$el.find('.datepicker').datepicker({dateFormat : 'dd-mm-yy'}); - - this.delegateEvents(); - - return this; - }, - - /** - * @returns {Function} from Handlebars - * @private - */ - template: function (data) { - if (!this._template) { - this._template = Handlebars.compile(TEMPLATE); - } - return this._template(data); - } - - }); - - OC.Share.ShareDialogExpirationView = ShareDialogExpirationView; - -})(); diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js index 5a78276a49191..925d8ed918e19 100644 --- a/core/js/sharedialoglinkshareview.js +++ b/core/js/sharedialoglinkshareview.js @@ -21,71 +21,101 @@ var TEMPLATE = '{{#if shareAllowed}}' + - '' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '{{#if singleAction}}' + - '' + - '{{else}}' + - '' + - '{{{popoverMenu}}}' + - '{{/if}}' + - '
' + - '{{#if publicUpload}}' + - '
' + - '' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '' + - '
' + - '{{/if}}' + - ' {{#if publicEditing}}' + - '
' + - ' ' + - ' ' + - '' + - '
' + - ' {{/if}}' + - ' {{#if showPasswordCheckBox}}' + - '' + - '' + - ' {{/if}}' + - '
' + - ' ' + - ' {{#if showPasswordCheckBox}}' + - ' ' + - ' {{else}}' + - ' ' + - ' {{/if}}' + - ' ' + - '
' + + '' + '{{else}}' + // FIXME: this doesn't belong in this view '{{#if noSharingPlaceholder}}{{/if}}' + '{{/if}}' ; var TEMPLATE_POPOVER_MENU = - '