diff options
author | Roeland Jago Douma <rullzer@users.noreply.github.com> | 2018-07-24 09:12:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-24 09:12:03 +0200 |
commit | b41d0d32e2183b8b241d3764f06a3b0f7d6b86c1 (patch) | |
tree | 915e14ceafed060f95255e8d2d7d4ad3ab3df7b5 | |
parent | 241e5705cb85d24a9c3479c133fcc4d9c069d8c7 (diff) | |
parent | fee62fd20b01b08ffbc7120720ba3bad0a89be73 (diff) | |
download | nextcloud-server-b41d0d32e2183b8b241d3764f06a3b0f7d6b86c1.tar.gz nextcloud-server-b41d0d32e2183b8b241d3764f06a3b0f7d6b86c1.zip |
Merge pull request #10218 from nextcloud/share-comments
allow to add a personal note to a share
42 files changed, 1252 insertions, 953 deletions
diff --git a/.gitignore b/.gitignore index c7b6b396635..a11e3a14597 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 e0c1bbfa095..f64a3702850 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 2a71af038cf..017253fdf8e 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 2e788a06c40..583912ad236 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 b4b64daff2b..83790c9ec4f 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 d30d5a05a22..33782d21b5f 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -144,6 +144,7 @@ class ShareAPIController extends OCSController { '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 @@ class ShareAPIController extends OCSController { * @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 @@ class ShareAPIController extends OCSController { 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 0b30a599c7f..bd1331a0908 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -262,6 +262,7 @@ class ShareController extends AuthPublicShareController { $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 476f0851547..81729c179fc 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -29,6 +29,12 @@ $maxUploadFilesize = min($upload_max_filesize, $post_max_size); <input type="hidden" name="filesize" value="<?php p($_['nonHumanFileSize']); ?>" id="filesize"> <?php endif; ?> <input type="hidden" name="maxSizeAnimateGif" value="<?php p($_['maxSizeAnimateGif']); ?>" id="maxSizeAnimateGif"> +<?php if (isset($_['note']) && $_['note'] !== '') : ?> + <div id="note"> + <?php p($l->t('Note:')); p(' ' . $_['note']); ?> + </div> +<?php endif; ?> + <?php if (!isset($_['hideFileList']) || (isset($_['hideFileList']) && $_['hideFileList'] === false)) { ?> <div id="files-public-content"> <div id="preview"> diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 5d376f2d4f7..30041c3a27b 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -238,7 +238,7 @@ class ShareAPIControllerTest extends TestCase { */ 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 @@ class ShareAPIControllerTest extends TestCase { $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 @@ class ShareAPIControllerTest extends TestCase { null, 6, 'target', - 0 + 0, + 'personal note' ); $expected = [ 'id' => 100, @@ -334,6 +336,7 @@ class ShareAPIControllerTest extends TestCase { 'storage' => 101, 'mail_send' => 0, 'uid_file_owner' => 'ownerId', + 'note' => 'personal note', 'displayname_file_owner' => 'ownerDisplay', 'mimetype' => 'myMimeType', ]; @@ -352,7 +355,8 @@ class ShareAPIControllerTest extends TestCase { null, 6, 'target', - 0 + 0, + 'personal note' ); $expected = [ 'id' => 101, @@ -376,6 +380,7 @@ class ShareAPIControllerTest extends TestCase { 'storage' => 101, 'mail_send' => 0, 'uid_file_owner' => 'ownerId', + 'note' => 'personal note', 'displayname_file_owner' => 'ownerDisplay', 'mimetype' => 'myFolderMimeType', ]; @@ -396,6 +401,7 @@ class ShareAPIControllerTest extends TestCase { 6, 'target', 0, + 'personal note', 'token', 'password' ); @@ -422,6 +428,7 @@ class ShareAPIControllerTest extends TestCase { 'mail_send' => 0, 'url' => 'url', 'uid_file_owner' => 'ownerId', + 'note' => 'personal note', 'displayname_file_owner' => 'ownerDisplay', 'mimetype' => 'myFolderMimeType', ]; @@ -455,7 +462,7 @@ class ShareAPIControllerTest extends TestCase { ->willReturn(true); $this->shareManager - ->expects($this->once()) + ->expects($this->any()) ->method('getShareById') ->with($share->getFullId(), 'currentUser') ->willReturn($share); @@ -501,6 +508,8 @@ class ShareAPIControllerTest extends TestCase { ['group', $group], ])); + $d = $ocs->getShare($share->getId())->getData()[0]; + $this->assertEquals($result, $ocs->getShare($share->getId())->getData()[0]); } @@ -1810,9 +1819,10 @@ class ShareAPIControllerTest extends TestCase { ->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 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { ->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 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { ]; // with existing group + $share = \OC::$server->getShareManager()->newShare(); $share->setShareType(\OCP\Share::SHARE_TYPE_GROUP) ->setSharedWith('recipientGroup') @@ -1924,6 +1937,7 @@ class ShareAPIControllerTest extends TestCase { ->setNode($file) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -1939,6 +1953,7 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { ->setNode($file) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ [ @@ -1978,6 +1994,7 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { ->setPassword('mypassword') ->setExpirationDate(new \DateTime('2001-01-02T00:00:00')) ->setToken('myToken') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -2019,6 +2037,7 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { ->setNode($folder) ->setShareTime(new \DateTime('2000-01-01T00:01:02')) ->setTarget('myTarget') + ->setNote('personal note') ->setId(42); $result[] = [ @@ -2059,6 +2079,7 @@ class ShareAPIControllerTest extends TestCase { '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 @@ class ShareAPIControllerTest extends TestCase { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2142,6 +2164,7 @@ class ShareAPIControllerTest extends TestCase { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2183,6 +2206,7 @@ class ShareAPIControllerTest extends TestCase { 'token' => null, 'uid_file_owner' => 'owner', 'displayname_file_owner' => 'owner', + 'note' => '', 'path' => 'folder', 'item_type' => 'folder', 'storage_id' => 'storageId', @@ -2207,6 +2231,7 @@ class ShareAPIControllerTest extends TestCase { ->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 @@ class ShareAPIControllerTest extends TestCase { '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 fb417878647..a01560d0288 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -192,6 +192,9 @@ class ShareControllerTest extends \Test\TestCase { public function testShowShare() { + + $note = 'personal note'; + $this->shareController->setToken('token'); $owner = $this->getMockBuilder(IUser::class)->getMock(); @@ -210,6 +213,7 @@ class ShareControllerTest extends \Test\TestCase { $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 @@ class ShareControllerTest extends \Test\TestCase { '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 1a1855b9c44..73e962e3292 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -506,6 +506,61 @@ class ShareByMailProvider implements IShareProvider { 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 @@ class ShareByMailProvider implements IShareProvider { ->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 @@ class ShareByMailProvider implements IShareProvider { ->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 95d746cfb46..f0d99e6026c 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -342,15 +342,17 @@ class ShareByMailProviderTest extends TestCase { $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 @@ class ShareByMailProviderTest extends TestCase { $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 @@ class ShareByMailProviderTest extends TestCase { $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 @@ class ShareByMailProviderTest extends TestCase { } ); - $this->assertInstanceOf('OCP\Share\IShare', - $instance->getShareByToken('token') - ); + $result = $instance->getShareByToken('token'); + + $this->assertInstanceOf('OCP\Share\IShare', $result); } /** @@ -511,7 +514,7 @@ class ShareByMailProviderTest extends TestCase { $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 @@ class ShareByMailProviderTest extends TestCase { $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 @@ class ShareByMailProviderTest extends TestCase { ->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 00000000000..268a479eaad --- /dev/null +++ b/core/Migrations/Version14000Date20180712153140.php @@ -0,0 +1,43 @@ +<?php +/** + * @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +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 d524dd94bb7..86f0e622405 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 00000000000..ec7f51065e4 --- /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 07489cd55a3..00000000000 --- a/core/css/share.scss +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @copyright Copyright (c) 2016, John Molakvoæ <skjnldsv@protonmail.com> - * @copyright Copyright (c) 2016, Morris Jobke <hey@morrisjobke.de> - * @copyright Copyright (c) 2016, Julia Bode <julia.bode@lulisaur.us> - * @copyright Copyright (c) 2016, Christoph Wurst <christoph@winzerhof-wurst.at> - * @copyright Copyright (c) 2015, Hendrik Leppelsack <hendrik@leppelsack.de> - * @copyright Copyright (c) 2015, Jan-Christoph Borchardt <hey@jancborchardt.net> - * @copyright Copyright (c) 2015, Vincent Petry <pvince81@owncloud.com> - * @copyright Copyright (c) 2015, Arthur Schiwon <blizzz@owncloud.com> - * @copyright Copyright (c) 2015, Roeland Jago Douma <roeland@famdouma.nl> - * @copyright Copyright (c) 2015, Morris Jobke <hey@morrisjobke.de> - * - * @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 30aa25d183c..9652b02e9d2 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 00000000000..d85defb6a09 --- /dev/null +++ b/core/img/actions/public-white.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" height="16" width="16"><path fill="#fff" d="m9.2363 2.166-3.1816 3.1836c-0.7071 0.7072-1.0378 1.6182-0.9883 2.457 0.05 0.8389 0.4333 1.5841 0.9883 2.1387l1.4121-1.416c-0.5672-0.5672-0.5444-1.2192 0.002-1.7656l3.1812-3.1817c0.52536-0.52536 1.2507-0.52318 1.772-0.002 0.48245 0.5556 0.52732 1.2382-0.004 1.7695l-0.82 0.8203c0.555 0.785 0.645 1.3663 0.593 2.2344l1.641-1.6406c1.2374-1.2374 1.2371-3.3645 0-4.6016-1.236-1.2361-3.342-1.2113-4.5957 0.004zm0.7071 3.8848-1.4141 1.418c0 0 0.003-00 0.004 0 0.55 0.55 0.50736 1.2582-0.004 1.7695l-3.1816 3.1817c-0.696 0.59192-1.2985 0.47105-1.7696 0-0.62636-0.62636-0.5-1.2681 0-1.768l0.85-0.8473c-0.556-0.7835-0.6484-1.365-0.5976-2.2324l-1.666 1.666c-1.2393 1.2393-1.2357 3.36 0 4.5957 1.2353 1.2353 3.362 1.2356 4.5976 0l3.1817-3.182c0.7086-0.7083 1.0396-1.6184 0.9906-2.4586-0.048-0.8401-0.432-1.5864-0.9887-2.1407z"/></svg> diff --git a/core/js/apps.js b/core/js/apps.js index b40883e88cf..473fec313a1 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 502e3a57976..2ebc2e710ed 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 d39945b8f79..63c3575a666 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 a9849ef9161..00000000000 --- 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}}' + - '<input type="checkbox" name="expirationCheckbox" class="expirationCheckbox checkbox" id="expirationCheckbox-{{cid}}" value="1" ' + - '{{#if isExpirationSet}}checked="checked"{{/if}} {{#if disableCheckbox}}disabled="disabled"{{/if}} />' + - '<label for="expirationCheckbox-{{cid}}">{{setExpirationLabel}}</label>' + - '<div class="expirationDateContainer {{#unless isExpirationSet}}hidden{{/unless}}">' + - ' <label for="expirationDate" class="hidden-visually" value="{{expirationDate}}">{{expirationLabel}}</label>' + - ' <input id="expirationDate" class="datepicker" type="text" placeholder="{{expirationDatePlaceholder}}" value="{{expirationValue}}" />' + - '</div>' + - ' {{#if isExpirationEnforced}}' + - // originally the expire message was shown when a default date was set, however it never had text - '<em id="defaultExpireMessage">{{defaultExpireMessage}}</em>' + - ' {{/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 5a78276a491..925d8ed918e 100644 --- a/core/js/sharedialoglinkshareview.js +++ b/core/js/sharedialoglinkshareview.js @@ -21,71 +21,101 @@ var TEMPLATE = '{{#if shareAllowed}}' + - '<span class="icon-loading-small hidden"></span>' + - '<input type="checkbox" name="linkCheckbox" id="linkCheckbox-{{cid}}" class="checkbox linkCheckbox" value="1" {{#if isLinkShare}}checked="checked"{{/if}} />' + - '<label for="linkCheckbox-{{cid}}">{{linkShareLabel}}</label>' + - '<br />' + - '<div class="oneline">' + - '<label for="linkText-{{cid}}" class="hidden-visually">{{urlLabel}}</label>' + - '<input id="linkText-{{cid}}" class="linkText {{#unless isLinkShare}}hidden{{/unless}}" type="text" readonly="readonly" value="{{shareLinkURL}}" />' + - '{{#if singleAction}}' + - '<a class="{{#unless isLinkShare}}hidden-visually{{/unless}} clipboardButton icon icon-clippy" data-clipboard-target="#linkText-{{cid}}"></a>' + - '{{else}}' + - '<a class="{{#unless isLinkShare}}hidden-visually{{/unless}}" href="#"><span class="linkMore icon icon-more"></span></a>' + - '{{{popoverMenu}}}' + - '{{/if}}' + - '</div>' + - '{{#if publicUpload}}' + - '<div>' + - '<span class="icon-loading-small hidden"></span>' + - '<input type="radio" name="publicUpload" value="{{publicUploadRValue}}" id="sharingDialogAllowPublicUpload-r-{{cid}}" class="radio publicUploadRadio" {{{publicUploadRChecked}}} />' + - '<label for="sharingDialogAllowPublicUpload-r-{{cid}}">{{publicUploadRLabel}}</label>' + - '</div>' + - '<div>' + - '<span class="icon-loading-small hidden"></span>' + - '<input type="radio" name="publicUpload" value="{{publicUploadRWValue}}" id="sharingDialogAllowPublicUpload-rw-{{cid}}" class="radio publicUploadRadio" {{{publicUploadRWChecked}}} />' + - '<label for="sharingDialogAllowPublicUpload-rw-{{cid}}">{{publicUploadRWLabel}}</label>' + - '</div>' + - '<div>' + - '<span class="icon-loading-small hidden"></span>' + - '<input type="radio" name="publicUpload" value="{{publicUploadWValue}}" id="sharingDialogAllowPublicUpload-w-{{cid}}" class="radio publicUploadRadio" {{{publicUploadWChecked}}} />' + - '<label for="sharingDialogAllowPublicUpload-w-{{cid}}">{{publicUploadWLabel}}</label>' + - '</div>' + - '{{/if}}' + - ' {{#if publicEditing}}' + - '<div id="allowPublicEditingWrapper">' + - ' <span class="icon-loading-small hidden"></span>' + - ' <input type="checkbox" value="1" name="allowPublicEditing" id="sharingDialogAllowPublicEditing-{{cid}}" class="checkbox publicEditingCheckbox" {{{publicEditingChecked}}} />' + - '<label for="sharingDialogAllowPublicEditing-{{cid}}">{{publicEditingLabel}}</label>' + - '</div>' + - ' {{/if}}' + - ' {{#if showPasswordCheckBox}}' + - '<input type="checkbox" name="showPassword" id="showPassword-{{cid}}" class="checkbox showPasswordCheckbox" {{#if isPasswordSet}}checked="checked"{{/if}} value="1" />' + - '<label for="showPassword-{{cid}}">{{enablePasswordLabel}}</label>' + - ' {{/if}}' + - '<div id="linkPass" class="oneline linkPass {{#unless isPasswordSet}}hidden{{/unless}}">' + - ' <label for="linkPassText-{{cid}}" class="hidden-visually">{{passwordLabel}}</label>' + - ' {{#if showPasswordCheckBox}}' + - ' <input id="linkPassText-{{cid}}" class="linkPassText" type="password" placeholder="{{passwordPlaceholder}}" autocomplete="new-password" />' + - ' {{else}}' + - ' <input id="linkPassText-{{cid}}" class="linkPassText" type="password" placeholder="{{passwordPlaceholderInitial}}" autocomplete="new-password" />' + - ' {{/if}}' + - ' <span class="icon icon-loading-small hidden"></span>' + - '</div>' + + '<ul id="shareLink" class="shareWithList">' + + ' <li data-share-id="{{cid}}">' + + ' <div class="avatar icon-public-white"></div><span class="username" title="{{linkShareLabel}}">{{linkShareLabel}}</span>' + + ' <span class="sharingOptionsGroup">' + + ' <span class="shareOption"> ' + + ' <span class="icon-loading-small hidden"></span>' + + ' <input id="linkCheckbox-{{cid}}" {{#if isLinkShare}}checked="checked"{{/if}} type="checkbox" name="linkCheckbox" class="linkCheckbox permissions checkbox">' + + ' <label for="linkCheckbox-{{cid}}">{{linkShareEnableLabel}}</label>' + + ' </span>' + + ' {{#if isLinkShare}}' + + ' <div class="share-menu" tabindex="0"><span class="icon icon-more"></span>' + + ' {{{popoverMenu}}}' + + ' </div>' + + ' {{/if}}' + + ' </span>' + + ' </li>' + + '</ul>' + '{{else}}' + // FIXME: this doesn't belong in this view '{{#if noSharingPlaceholder}}<input id="shareWith-{{cid}}" class="shareWithField" type="text" placeholder="{{noSharingPlaceholder}}" disabled="disabled"/>{{/if}}' + '{{/if}}' ; var TEMPLATE_POPOVER_MENU = - '<div class="popovermenu bubble hidden menu socialSharingMenu">' + + '<div class="popovermenu menu">' + '<ul>' + '<li>' + - '<a href="#" class="shareOption menuitem clipboardButton" data-clipboard-target="#linkText-{{cid}}">' + + '<a href="#" class="menuitem clipboardButton" data-clipboard-text="{{shareLinkURL}}">' + '<span class="icon icon-clippy" ></span>' + '<span>{{copyLabel}}</span>' + '</a>' + '</li>' + + '<li class="hidden linkTextMenu">' + + '<span class="menuitem icon-link-text">' + + '<input id="linkText-{{cid}}" class="linkText" type="text" readonly="readonly" value="{{shareLinkURL}}" />' + + '</span>' + + '</li>' + + '{{#if publicUpload}}' + + '<li><span class="shareOption menuitem">' + + '<span class="icon-loading-small hidden"></span>' + + '<input type="radio" name="publicUpload" value="{{publicUploadRValue}}" id="sharingDialogAllowPublicUpload-r-{{cid}}" class="radio publicUploadRadio" {{{publicUploadRChecked}}} />' + + '<label for="sharingDialogAllowPublicUpload-r-{{cid}}">{{publicUploadRLabel}}</label>' + + '</span></li>' + + '<li><span class="shareOption menuitem">' + + '<span class="icon-loading-small hidden"></span>' + + '<input type="radio" name="publicUpload" value="{{publicUploadRWValue}}" id="sharingDialogAllowPublicUpload-rw-{{cid}}" class="radio publicUploadRadio" {{{publicUploadRWChecked}}} />' + + '<label for="sharingDialogAllowPublicUpload-rw-{{cid}}">{{publicUploadRWLabel}}</label>' + + '</span></li>' + + '<li><span class="shareOption menuitem">' + + '<span class="icon-loading-small hidden"></span>' + + '<input type="radio" name="publicUpload" value="{{publicUploadWValue}}" id="sharingDialogAllowPublicUpload-w-{{cid}}" class="radio publicUploadRadio" {{{publicUploadWChecked}}} />' + + '<label for="sharingDialogAllowPublicUpload-w-{{cid}}">{{publicUploadWLabel}}</label>' + + '</span></li>' + + '{{/if}}' + + '{{#if publicEditing}}' + + ' <li id="allowPublicEditingWrapper"><span class="shareOption menuitem">' + + ' <span class="icon-loading-small hidden"></span>' + + ' <input type="checkbox" name="allowPublicEditing" id="sharingDialogAllowPublicEditing-{{cid}}" class="checkbox publicEditingCheckbox" {{{publicEditingChecked}}} />' + + ' <label for="sharingDialogAllowPublicEditing-{{cid}}">{{publicEditingLabel}}</label>' + + ' </span></li>' + + '{{/if}}' + + '{{#if showPasswordCheckBox}}' + + ' <li><span class="shareOption menuitem">' + + ' <input type="checkbox" name="showPassword" id="showPassword-{{cid}}" class="checkbox showPasswordCheckbox" {{#if isPasswordSet}}checked="checked"{{/if}} value="1" />' + + ' <label for="showPassword-{{cid}}">{{enablePasswordLabel}}</label>' + + ' </span></li>' + + ' <li class="{{#unless isPasswordSet}}hidden{{/unless}} linkPassMenu"><span class="shareOption menuitem icon-share-pass">' + + ' <input id="linkPassText-{{cid}}" class="linkPassText" type="password" placeholder="{{passwordPlaceholder}}" autocomplete="new-password" />' + + ' <span class="icon icon-loading-small hidden"></span>' + + ' </span></li>' + + '{{/if}}' + + '<li>' + + '<span class="shareOption menuitem">' + + '<input id="expireDate-{{cid}}" type="checkbox" name="expirationDate" class="expireDate checkbox" {{#if hasExpireDate}}checked="checked"{{/if}}" />' + + '<label for="expireDate-{{cid}}">{{expireDateLabel}}</label>' + + '</li>' + + '<li class="{{#unless hasExpireDate}}hidden{{/unless}}">' + + '<span class="menuitem icon-expiredate expirationDateContainer-{{cid}}">' + + ' <label for="expirationDatePicker-{{cid}}" class="hidden-visually" value="{{expirationDate}}">{{expirationLabel}}</label>' + + ' <input id="expirationDatePicker-{{cid}}" class="datepicker" type="text" placeholder="{{expirationDatePlaceholder}}" value="{{#if hasExpireDate}}{{expireDate}}{{else}}{{defaultExpireDate}}{{/if}}" />' + + '</span>' + + '</span>' + + '</li>' + + '<li>' + + '<a href="#" class="share-add"><span class="icon-loading-small hidden"></span>' + + ' <span class="icon icon-edit"></span>' + + ' <span>{{addNoteLabel}}</span>' + + ' <input type="button" class="share-note-delete icon-delete">' + + '</a>' + + '</li>' + + '<li class="share-note-form share-note-link hidden">' + + '<span class="menuitem icon-note">' + + ' <textarea class="share-note">{{shareNote}}</textarea>' + + ' <input type="submit" class="icon-confirm share-note-submit" value="" id="add-note-{{shareId}}" />' + + '</span>' + + '</li>' + '{{#each social}}' + '<li>' + '<a href="#" class="shareOption menuitem pop-up" data-url="{{url}}" data-window="{{newWindow}}">' + @@ -131,9 +161,15 @@ 'click .linkText': 'onLinkTextClick', 'change .publicEditingCheckbox': 'onAllowPublicEditingChange', 'click .showPasswordCheckbox': 'onShowPasswordClick', - 'click .icon-more': 'onToggleMenu', + 'click .share-menu .icon-more': 'onToggleMenu', 'click .pop-up': 'onPopUpClick', - 'change .publicUploadRadio': 'onPublicUploadChange' + 'change .publicUploadRadio': 'onPublicUploadChange', + 'click .expireDate' : 'onExpireDateChange', + 'change .datepicker': 'onChangeExpirationDate', + 'click .datepicker' : 'showDatePicker', + 'click .share-add': 'showNoteForm', + 'click .share-note-delete': 'deleteNote', + 'click .share-note-submit': 'updateNote' }, initialize: function(options) { @@ -165,37 +201,26 @@ throw 'missing OC.Share.ShareConfigModel'; } - _.bindAll( - this, - 'onLinkCheckBoxChange', - 'onPasswordEntered', - 'onPasswordKeyUp', - 'onLinkTextClick', - 'onShowPasswordClick', - 'onAllowPublicEditingChange', - 'onPublicUploadChange' - ); - var clipboard = new Clipboard('.clipboardButton'); clipboard.on('success', function(e) { - var $input = $(e.trigger); - $input.tooltip('hide') + var $menu = $(e.trigger); + var $linkTextMenu = $menu.parent().next('li.linkTextMenu') + + $menu.tooltip('hide') .attr('data-original-title', t('core', 'Copied!')) .tooltip('fixTitle') .tooltip({placement: 'bottom', trigger: 'manual'}) .tooltip('show'); _.delay(function() { - $input.tooltip('hide'); - if (OC.Share.Social.Collection.size() == 0) { - $input.attr('data-original-title', t('core', 'Copy')) - .tooltip('fixTitle'); - } else { - $input.tooltip("destroy"); - } + $menu.tooltip('hide'); + $menu.tooltip('destroy'); }, 3000); }); clipboard.on('error', function (e) { - var $input = $(e.trigger); + var $menu = $(e.trigger); + var $linkTextMenu = $menu.parent().next('li.linkTextMenu') + var $input = $linkTextMenu.find('.linkText'); + var actionMsg = ''; if (/iPhone|iPad/i.test(navigator.userAgent)) { actionMsg = t('core', 'Not supported!'); @@ -205,6 +230,8 @@ actionMsg = t('core', 'Press Ctrl-C to copy.'); } + $linkTextMenu.removeClass('hidden'); + $input.select(); $input.tooltip('hide') .attr('data-original-title', actionMsg) .tooltip('fixTitle') @@ -212,15 +239,10 @@ .tooltip('show'); _.delay(function () { $input.tooltip('hide'); - if (OC.Share.Social.Collection.size() == 0) { - $input.attr('data-original-title', t('core', 'Copy')) - .tooltip('fixTitle'); - } else { - $input.tooltip("destroy"); - } + $input.attr('data-original-title', t('core', 'Copy')) + .tooltip('fixTitle'); }, 3000); }); - }, onLinkCheckBoxChange: function() { @@ -257,6 +279,7 @@ onShowPasswordClick: function() { this.$el.find('.linkPass').slideToggle(OC.menuSpeed); + this.$el.find('.linkPassMenu').toggleClass('hidden'); if(!this.$el.find('.showPasswordCheckbox').is(':checked')) { this.model.saveLinkShare({ password: '' @@ -275,7 +298,7 @@ }, onPasswordEntered: function() { - var $loading = this.$el.find('.linkPass .icon-loading-small'); + var $loading = this.$el.find('.linkPassMenu .icon-loading-small'); if (!$loading.hasClass('hidden')) { // still in process return; @@ -334,12 +357,95 @@ }, - onPublicUploadChange: function(e) { + onPublicUploadChange: function(e) { var permissions = e.currentTarget.value; this.model.saveLinkShare({ permissions: permissions }); }, + + showNoteForm: function(event) { + event.preventDefault(); + event.stopPropagation(); + var self = this; + var $element = $(event.target); + var $li = $element.closest('li[data-share-id]'); + var $menu = $element.closest('li'); + var $form = $menu.next('li.share-note-form'); + + // show elements + $menu.find('.share-note-delete').toggle(); + $form.toggleClass('hidden'); + $form.find('textarea').focus(); + }, + + deleteNote: function(event) { + event.preventDefault(); + event.stopPropagation(); + var self = this; + var $element = $(event.target); + var $li = $element.closest('li[data-share-id]'); + var shareId = $li.data('share-id'); + var $menu = $element.closest('li'); + var $form = $menu.next('li.share-note-form'); + + console.log($form.find('.share-note')); + $form.find('.share-note').val(''); + + $form.addClass('hidden'); + $menu.find('.share-note-delete').hide(); + + self.sendNote('', shareId, $menu); + }, + + updateNote: function(event) { + event.preventDefault(); + event.stopPropagation(); + var self = this; + var $element = $(event.target); + var $li = $element.closest('li[data-share-id]'); + var shareId = $li.data('share-id'); + var $form = $element.closest('li.share-note-form'); + var $menu = $form.prev('li'); + var message = $form.find('.share-note').val().trim(); + + if (message.length < 1) { + return; + } + + self.sendNote(message, shareId, $menu); + }, + + sendNote: function(note, shareId, $menu) { + var $form = $menu.next('li.share-note-form'); + var $submit = $form.find('input.share-note-submit'); + var $error = $form.find('input.share-note-error'); + + $submit.prop('disabled', true); + $menu.find('.icon-loading-small').removeClass('hidden'); + $menu.find('.icon-edit').hide(); + + var complete = function() { + $submit.prop('disabled', false); + $menu.find('.icon-loading-small').addClass('hidden'); + $menu.find('.icon-edit').show(); + }; + var error = function() { + $error.show(); + setTimeout(function() { + $error.hide(); + }, 3000); + }; + + // send data + $.ajax({ + method: 'PUT', + url: OC.linkToOCS('apps/files_sharing/api/v1/shares',2) + shareId + '?' + OC.buildQueryString({format: 'json'}), + data: { note: note }, + complete : complete, + error: error + }); + }, render: function() { var linkShareTemplate = this.template(); @@ -412,18 +518,48 @@ }); }); - var popover = this.popoverMenuTemplate({ - cid: this.cid, - copyLabel: t('core', 'Copy'), - social: social + var defaultExpireDays = this.configModel.get('defaultExpireDate'); + var isExpirationEnforced = this.configModel.get('isDefaultExpireDateEnforced'); + var hasExpireDate = !!this.model.get('linkShare').expiration || isExpirationEnforced; + + var expireDate; + if (hasExpireDate) { + expireDate = moment(this.model.get('linkShare').expiration, 'YYYY-MM-DD').format('DD-MM-YYYY'); + } + + // 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(hasExpireDate) { + 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.html(linkShareTemplate({ - cid: this.cid, - shareAllowed: true, - isLinkShare: isLinkShare, + this.$el.find('.datepicker').datepicker({dateFormat : 'dd-mm-yy'}); + + var popover = this.popoverMenuTemplate({ + cid: this.model.get('linkShare').id, + copyLabel: t('core', 'Copy URL'), + social: social, + shareLinkURL: this.model.get('linkShare').link, - linkShareLabel: t('core', 'Share link'), urlLabel: t('core', 'Link'), enablePasswordLabel: t('core', 'Password protect'), passwordLabel: t('core', 'Password'), @@ -437,8 +573,6 @@ publicEditingLabel: t('core', 'Allow editing'), mailPrivatePlaceholder: t('core', 'Email link to person'), mailButtonText: t('core', 'Send'), - singleAction: OC.Share.Social.Collection.size() == 0, - popoverMenu: popover, publicUploadRWLabel: t('core', 'Allow upload and editing'), publicUploadRLabel: t('core', 'Read only'), publicUploadWLabel: t('core', 'File drop (upload only)'), @@ -447,19 +581,32 @@ publicUploadWValue: OC.PERMISSION_CREATE, publicUploadRWChecked: publicUploadRWChecked, publicUploadRChecked: publicUploadRChecked, - publicUploadWChecked: publicUploadWChecked - })); + publicUploadWChecked: publicUploadWChecked, + expireDateLabel: t('core', 'Set expiration date'), + expirationLabel: t('core', 'Expiration'), + expirationDatePlaceholder: t('core', 'Expiration date'), + hasExpireDate: hasExpireDate, + isExpirationEnforced: isExpirationEnforced, + expireDate: expireDate, + defaultExpireDate: moment().add(1, 'day').format('DD-MM-YYYY'), // Can't expire today + shareNote: this.model.get('linkShare').note, + addNoteLabel: t('core', 'Set share note'), + }); - if (OC.Share.Social.Collection.size() == 0) { - this.$el.find('.clipboardButton').tooltip({ - placement: 'bottom', - title: t('core', 'Copy'), - trigger: 'hover' - }); - } + this.$el.html(linkShareTemplate({ + cid: this.model.get('linkShare').id, + shareAllowed: true, + isLinkShare: isLinkShare, + linkShareLabel: t('core', 'Share link'), + linkShareEnableLabel: t('core', 'Enable'), + popoverMenu: popover, + })); this.delegateEvents(); + // new note autosize + autosize(this.$el.find('.share-note-form .share-note')); + return this; }, @@ -467,8 +614,8 @@ event.preventDefault(); event.stopPropagation(); var $element = $(event.target); - var $li = $element.closest('.oneline'); - var $menu = $li.find('.popovermenu'); + var $li = $element.closest('li[data-share-id]'); + var $menu = $li.find('.sharingOptionsGroup .popovermenu'); OC.showMenu(null, $menu); this._menuOpen = $li.data('share-id'); @@ -517,7 +664,52 @@ window.location.href = url; } } - } + }, + + onExpireDateChange: function(event) { + var $element = $(event.target); + var li = $element.closest('li[data-share-id]'); + var shareId = li.data('share-id'); + var expirationDatePicker = '#expirationDateContainer-' + shareId; + var datePicker = $(expirationDatePicker); + var state = $element.prop('checked'); + datePicker.toggleClass('hidden', !state); + + if (!state) { + // disabled, let's hide the input and + // set the expireDate to nothing + $element.closest('li').next('li').addClass('hidden'); + this.setExpirationDate(''); + } else { + // enabled, show the input and the datepicker + $element.closest('li').next('li').removeClass('hidden'); + this.showDatePicker(event); + + } + }, + + showDatePicker: function(event) { + var $element = $(event.target); + var li = $element.closest('li[data-share-id]'); + var shareId = li.data('share-id'); + var expirationDatePicker = '#expirationDatePicker-' + shareId; + var self = this; + + $(expirationDatePicker).datepicker({ + dateFormat : 'dd-mm-yy', + onSelect: function (expireDate) { + self.setExpirationDate(expireDate); + } + }); + console.log(event, $(expirationDatePicker)); + $(expirationDatePicker).datepicker('show'); + $(expirationDatePicker).focus(); + + }, + + setExpirationDate: function(expireDate) { + this.model.saveLinkShare({expireDate: expireDate}); + }, }); diff --git a/core/js/sharedialogresharerinfoview.js b/core/js/sharedialogresharerinfoview.js index f4bf9afa0b2..fadd0a41f7b 100644 --- a/core/js/sharedialogresharerinfoview.js +++ b/core/js/sharedialogresharerinfoview.js @@ -19,7 +19,8 @@ '<span class="reshare">' + ' <div class="avatar" data-userName="{{reshareOwner}}"></div>' + ' {{sharedByText}}' + - '</span><br/>' + '</span>' + + '{{#if hasShareNote}}<div class="share-note">{{shareNote}}</div>{{/if}}' ; /** @@ -72,7 +73,10 @@ var reshareTemplate = this.template(); var ownerDisplayName = this.model.getReshareOwnerDisplayname(); + var shareNote = this.model.getReshareNote(); + var sharedByText = ''; + if (this.model.getReshareType() === OC.Share.SHARE_TYPE_GROUP) { sharedByText = t( 'core', @@ -105,9 +109,13 @@ ); } + + this.$el.html(reshareTemplate({ reshareOwner: this.model.getReshareOwner(), - sharedByText: sharedByText + sharedByText: sharedByText, + shareNote: shareNote, + hasShareNote: shareNote !== '' })); this.$el.find('.avatar').each(function() { diff --git a/core/js/sharedialogshareelistview.js b/core/js/sharedialogshareelistview.js index 53a65fcdf7a..574d47b4aac 100644 --- a/core/js/sharedialogshareelistview.js +++ b/core/js/sharedialogshareelistview.js @@ -34,8 +34,9 @@ '<label for="canEdit-{{cid}}-{{shareId}}">{{canEditLabel}}</label>' + '</span>' + '{{/if}}' + - '<a href="#"><span class="icon icon-more"></span></a>' + - '{{{popoverMenu}}}' + + '<div tabindex="0" class="share-menu"><span class="icon icon-more"></span>' + + '{{{popoverMenu}}}' + + '</div>' + '</span>' + '</li>' + '{{/each}}' + @@ -102,11 +103,13 @@ '<span class="shareOption menuitem">' + '<input id="password-{{cid}}-{{shareId}}" type="checkbox" name="password" class="password checkbox" {{#if isPasswordSet}}checked="checked"{{/if}}{{#if isPasswordSet}}{{#if isPasswordForMailSharesRequired}}disabled=""{{/if}}{{/if}}" />' + '<label for="password-{{cid}}-{{shareId}}">{{passwordLabel}}</label>' + - '<div class="passwordContainer-{{cid}}-{{shareId}} {{#unless isPasswordSet}}hidden{{/unless}}">' + - ' <label for="passwordField-{{cid}}-{{shareId}}" class="hidden-visually" value="{{password}}">{{passwordLabel}}</label>' + - ' <input id="passwordField-{{cid}}-{{shareId}}" class="passwordField" type="password" placeholder="{{passwordPlaceholder}}" value="{{passwordValue}}" autocomplete="new-password" />' + - ' <span class="icon-loading-small hidden"></span>' + - '</div>' + + '</span>' + + '</li>' + + '<li class="passwordMenu-{{cid}}-{{shareId}} {{#unless isPasswordSet}}hidden{{/unless}}">' + + '<span class="passwordContainer-{{cid}}-{{shareId}} icon-passwordmail menuitem">' + + ' <label for="passwordField-{{cid}}-{{shareId}}" class="hidden-visually" value="{{password}}">{{passwordLabel}}</label>' + + ' <input id="passwordField-{{cid}}-{{shareId}}" class="passwordField" type="password" placeholder="{{passwordPlaceholder}}" value="{{passwordValue}}" autocomplete="new-password" />' + + ' <span class="icon-loading-small hidden"></span>' + '</span>' + '</li>' + '{{/if}}' + @@ -114,12 +117,29 @@ '<span class="shareOption menuitem">' + '<input id="expireDate-{{cid}}-{{shareId}}" type="checkbox" name="expirationDate" class="expireDate checkbox" {{#if hasExpireDate}}checked="checked"{{/if}}" />' + '<label for="expireDate-{{cid}}-{{shareId}}">{{expireDateLabel}}</label>' + - '<div class="expirationDateContainer-{{cid}}-{{shareId}} {{#unless hasExpireDate}}hidden{{/unless}}">' + - ' <label for="expirationDatePicker-{{cid}}-{{shareId}}" class="hidden-visually" value="{{expirationDate}}">{{expirationLabel}}</label>' + - ' <input id="expirationDatePicker-{{cid}}-{{shareId}}" class="datepicker" type="text" placeholder="{{expirationDatePlaceholder}}" value="{{#if hasExpireDate}}{{expireDate}}{{else}}{{defaultExpireDate}}{{/if}}" />' + - '</div>' + '</span>' + '</li>' + + '<li class="expirationDateMenu-{{cid}}-{{shareId}} {{#unless hasExpireDate}}hidden{{/unless}}">' + + '<span class="expirationDateContainer-{{cid}}-{{shareId}} icon-expiredate menuitem">' + + ' <label for="expirationDatePicker-{{cid}}-{{shareId}}" class="hidden-visually" value="{{expirationDate}}">{{expirationLabel}}</label>' + + ' <input id="expirationDatePicker-{{cid}}-{{shareId}}" class="datepicker" type="text" placeholder="{{expirationDatePlaceholder}}" value="{{#if hasExpireDate}}{{expireDate}}{{else}}{{defaultExpireDate}}{{/if}}" />' + + '</span>' + + '</li>' + + '{{#if isNoteAvailable}}' + + '<li>' + + '<a href="#" class="share-add"><span class="icon-loading-small hidden"></span>' + + ' <span class="icon icon-edit"></span>' + + ' <span>{{addNoteLabel}}</span>' + + ' <input type="button" class="share-note-delete icon-delete">' + + '</a>' + + '</li>' + + '<li class="share-note-form hidden">' + + '<span class="menuitem icon-note">' + + ' <textarea class="share-note">{{shareNote}}</textarea>' + + ' <input type="submit" class="icon-confirm share-note-submit" value="" id="add-note-{{shareId}}" />' + + '</span>' + + '</li>' + + '{{/if}}' + '<li>' + '<a href="#" class="unshare"><span class="icon-loading-small hidden"></span><span class="icon icon-delete"></span><span>{{unshareLabel}}</span></a>' + '</li>' + @@ -156,7 +176,10 @@ events: { 'click .unshare': 'onUnshare', - 'click .icon-more': 'onToggleMenu', + 'click .share-add': 'showNoteForm', + 'click .share-note-delete': 'deleteNote', + 'click .share-note-submit': 'updateNote', + 'click .share-menu .icon-more': 'onToggleMenu', 'click .permissions': 'onPermissionChange', 'click .expireDate' : 'onExpireDateChange', 'click .password' : 'onMailSharePasswordProtectChange', @@ -255,12 +278,14 @@ modSeed: shareType !== OC.Share.SHARE_TYPE_USER && (shareType !== OC.Share.SHARE_TYPE_CIRCLE || shareWithAvatar), isRemoteShare: shareType === OC.Share.SHARE_TYPE_REMOTE, isRemoteGroupShare: shareType === OC.Share.SHARE_TYPE_REMOTE_GROUP, + isNoteAvailable: shareType !== OC.Share.SHARE_TYPE_REMOTE && shareType !== OC.Share.SHARE_TYPE_REMOTE_GROUP, isMailShare: shareType === OC.Share.SHARE_TYPE_EMAIL, isCircleShare: shareType === OC.Share.SHARE_TYPE_CIRCLE, isFileSharedByMail: shareType === OC.Share.SHARE_TYPE_EMAIL && !this.model.isFolder(), isPasswordSet: hasPassword, secureDropMode: !this.model.hasReadPermission(shareIndex), hasExpireDate: this.model.getExpireDate(shareIndex) !== null, + shareNote: this.model.getNote(shareIndex), expireDate: moment(this.model.getExpireDate(shareIndex), 'YYYY-MM-DD').format('DD-MM-YYYY'), passwordPlaceholder: hasPassword ? PASSWORD_PLACEHOLDER : PASSWORD_PLACEHOLDER_MESSAGE, }); @@ -269,6 +294,7 @@ getShareProperties: function() { return { unshareLabel: t('core', 'Unshare'), + addNoteLabel: t('core', 'Set share note'), canShareLabel: t('core', 'Can reshare'), canEditLabel: t('core', 'Can edit'), createPermissionLabel: t('core', 'Can create'), @@ -435,6 +461,9 @@ this._renderPermissionChange = false; + // new note autosize + autosize(this.$el.find('.share-note-form .share-note')); + this.delegateEvents(); return this; @@ -470,6 +499,88 @@ return this._popoverMenuTemplate(data); }, + showNoteForm: function(event) { + event.preventDefault(); + event.stopPropagation(); + var $element = $(event.target); + var $menu = $element.closest('li'); + var $form = $menu.next('li.share-note-form'); + + // show elements + $menu.find('.share-note-delete').toggle(); + $form.toggleClass('hidden'); + $form.find('textarea').focus(); + }, + + deleteNote: function(event) { + event.preventDefault(); + event.stopPropagation(); + var self = this; + var $element = $(event.target); + var $li = $element.closest('li[data-share-id]'); + var shareId = $li.data('share-id'); + var $menu = $element.closest('li'); + var $form = $menu.next('li.share-note-form'); + + console.log($form.find('.share-note')); + $form.find('.share-note').val(''); + + $form.addClass('hidden'); + $menu.find('.share-note-delete').hide(); + + self.sendNote('', shareId, $menu); + }, + + updateNote: function(event) { + event.preventDefault(); + event.stopPropagation(); + var self = this; + var $element = $(event.target); + var $li = $element.closest('li[data-share-id]'); + var shareId = $li.data('share-id'); + var $form = $element.closest('li.share-note-form'); + var $menu = $form.prev('li'); + var message = $form.find('.share-note').val().trim(); + + if (message.length < 1) { + return; + } + + self.sendNote(message, shareId, $menu); + + }, + + sendNote: function(note, shareId, $menu) { + var $form = $menu.next('li.share-note-form'); + var $submit = $form.find('input.share-note-submit'); + var $error = $form.find('input.share-note-error'); + + $submit.prop('disabled', true); + $menu.find('.icon-loading-small').removeClass('hidden'); + $menu.find('.icon-edit').hide(); + + var complete = function() { + $submit.prop('disabled', false); + $menu.find('.icon-loading-small').addClass('hidden'); + $menu.find('.icon-edit').show(); + }; + var error = function() { + $error.show(); + setTimeout(function() { + $error.hide(); + }, 3000); + }; + + // send data + $.ajax({ + method: 'PUT', + url: OC.linkToOCS('apps/files_sharing/api/v1/shares',2) + shareId + '?' + OC.buildQueryString({format: 'json'}), + data: { note: note }, + complete : complete, + error: error + }); + }, + onUnshare: function(event) { event.preventDefault(); event.stopPropagation(); @@ -513,16 +624,21 @@ }, onExpireDateChange: function(event) { - var element = $(event.target); - var li = element.closest('li[data-share-id]'); + var $element = $(event.target); + var li = $element.closest('li[data-share-id]'); var shareId = li.data('share-id'); var datePickerClass = '.expirationDateContainer-' + this.cid + '-' + shareId; var datePicker = $(datePickerClass); - var state = element.prop('checked'); + var state = $element.prop('checked'); datePicker.toggleClass('hidden', !state); if (!state) { + // disabled, let's hide the input and + // set the expireDate to nothing + $element.closest('li').next('li').addClass('hidden'); this.setExpirationDate(shareId, ''); } else { + // enabled, show the input and the datepicker + $element.closest('li').next('li').removeClass('hidden'); this.showDatePicker(event); } @@ -552,7 +668,7 @@ var element = $(event.target); var li = element.closest('li[data-share-id]'); var shareId = li.data('share-id'); - var passwordContainerClass = '.passwordContainer-' + this.cid + '-' + shareId; + var passwordContainerClass = '.passwordMenu-' + this.cid + '-' + shareId; var passwordContainer = $(passwordContainerClass); var loading = this.$el.find(passwordContainerClass + ' .icon-loading-small'); var inputClass = '#passwordField-' + this.cid + '-' + shareId; diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js index d886e45856f..9fa1847d3d5 100644 --- a/core/js/sharedialogview.js +++ b/core/js/sharedialogview.js @@ -27,7 +27,6 @@ '{{/if}}' + '<div class="shareeListView subView"></div>' + '<div class="linkShareView subView"></div>' + - '<div class="expirationView subView"></div>' + '<div class="loading hidden" style="height: 50px"></div>'; /** @@ -60,9 +59,6 @@ linkShareView: undefined, /** @type {object} **/ - expirationView: undefined, - - /** @type {object} **/ shareeListView: undefined, /** @type {object} **/ @@ -111,7 +107,6 @@ var subViews = { resharerInfoView: 'ShareDialogResharerInfoView', linkShareView: 'ShareDialogLinkShareView', - expirationView: 'ShareDialogExpirationView', shareeListView: 'ShareDialogShareeListView' }; @@ -671,9 +666,6 @@ this.linkShareView.$el = this.$el.find('.linkShareView'); this.linkShareView.render(); - this.expirationView.$el = this.$el.find('.expirationView'); - this.expirationView.render(); - this.shareeListView.$el = this.$el.find('.shareeListView'); this.shareeListView.render(); diff --git a/core/js/shareitemmodel.js b/core/js/shareitemmodel.js index 93feba9c889..241b9c19c0d 100644 --- a/core/js/shareitemmodel.js +++ b/core/js/shareitemmodel.js @@ -343,6 +343,13 @@ /** * @returns {string} */ + getReshareNote: function() { + return this.get('reshare').note; + }, + + /** + * @returns {string} + */ getReshareWith: function() { return this.get('reshare').share_with; }, @@ -366,6 +373,10 @@ return this._shareExpireDate(shareIndex); }, + getNote: function(shareIndex) { + return this._shareNote(shareIndex); + }, + /** * Returns all share entries that only apply to the current item * (file/folder) @@ -502,6 +513,15 @@ return date2; }, + + _shareNote: function(shareIndex) { + var share = this.get('shares')[shareIndex]; + if(!_.isObject(share)) { + throw "Unknown Share"; + } + return share.note; + }, + /** * @return {int} */ diff --git a/core/js/tests/specs/sharedialoglinkshareview.js b/core/js/tests/specs/sharedialoglinkshareview.js index 12f5e762cee..9d07dcb479d 100644 --- a/core/js/tests/specs/sharedialoglinkshareview.js +++ b/core/js/tests/specs/sharedialoglinkshareview.js @@ -92,7 +92,7 @@ describe('OC.Share.ShareDialogLinkShareView', function () { var $passwordDiv = view.$el.find('#linkPass'); $passwordText = view.$el.find('.linkPassText'); - $workingIcon = view.$el.find('.linkPass .icon-loading-small'); + $workingIcon = view.$el.find('.linkPassMenu .icon-loading-small'); sinon.stub(shareModel, 'saveLinkShare'); diff --git a/core/js/tests/specs/sharedialogviewSpec.js b/core/js/tests/specs/sharedialogviewSpec.js index 5fd920a758c..33d3be51440 100644 --- a/core/js/tests/specs/sharedialogviewSpec.js +++ b/core/js/tests/specs/sharedialogviewSpec.js @@ -214,188 +214,6 @@ describe('OC.Share.ShareDialogView', function() { focusStub.restore(); selectStub.restore(); }); - describe('password', function() { - var slideToggleStub; - - beforeEach(function() { - $('#allowShareWithLink').val('yes'); - configModel.set({ - enforcePasswordForPublicLink: false - }); - - slideToggleStub = sinon.stub($.fn, 'slideToggle'); - }); - afterEach(function() { - slideToggleStub.restore(); - }); - - it('enforced but toggled does not fire request', function() { - configModel.set('enforcePasswordForPublicLink', true); - dialog.render(); - - dialog.$el.find('.linkCheckbox').click(); - - // The password linkPass field is shown (slideToggle is called). - // No request is made yet - expect(slideToggleStub.callCount).toEqual(1); - expect(slideToggleStub.getCall(0).thisValue.eq(0).attr('id')).toEqual('linkPass'); - expect(fakeServer.requests.length).toEqual(0); - - // Now untoggle share by link - dialog.$el.find('.linkCheckbox').click(); - dialog.render(); - - // Password field disappears and no ajax requests have been made - expect(fakeServer.requests.length).toEqual(0); - expect(slideToggleStub.callCount).toEqual(2); - expect(slideToggleStub.getCall(1).thisValue.eq(0).attr('id')).toEqual('linkPass'); - }); - }); - describe('expiration date', function() { - var shareData; - var shareItem; - var clock; - var expectedMinDate; - - beforeEach(function() { - // pick a fake date - clock = sinon.useFakeTimers(new Date(2014, 0, 20, 14, 0, 0).getTime()); - expectedMinDate = new Date(2014, 0, 21, 14, 0, 0); - - configModel.set({ - enforcePasswordForPublicLink: false, - isDefaultExpireDateEnabled: false, - isDefaultExpireDateEnforced: false, - defaultExpireDate: 7 - }); - - shareModel.set('linkShare', { - isLinkShare: true, - token: 'tehtoken', - permissions: OC.PERMISSION_READ, - expiration: null - }); - }); - afterEach(function() { - clock.restore(); - }); - - it('does not check expiration date checkbox when no date was set', function() { - shareModel.get('linkShare').expiration = null; - dialog.render(); - - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(false); - expect(dialog.$el.find('.datepicker').val()).toEqual(''); - }); - it('does not check expiration date checkbox for new share', function() { - dialog.render(); - - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(false); - expect(dialog.$el.find('.datepicker').val()).toEqual(''); - }); - it('checks expiration date checkbox and populates field when expiration date was set', function() { - shareModel.get('linkShare').expiration = '2014-02-01 00:00:00'; - dialog.render(); - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(true); - expect(dialog.$el.find('.datepicker').val()).toEqual('01-02-2014'); - }); - it('sets default date when default date setting is enabled', function() { - configModel.set('isDefaultExpireDateEnabled', true); - dialog.render(); - dialog.$el.find('.linkCheckbox').click(); - // here fetch would be called and the server returns the expiration date - shareModel.get('linkShare').expiration = '2014-1-27 00:00:00'; - dialog.render(); - - // enabled by default - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(true); - expect(dialog.$el.find('.datepicker').val()).toEqual('27-01-2014'); - - // disabling is allowed - dialog.$el.find('[name=expirationCheckbox]').click(); - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(false); - }); - it('enforces default date when enforced date setting is enabled', function() { - configModel.set({ - isDefaultExpireDateEnabled: true, - isDefaultExpireDateEnforced: true - }); - dialog.render(); - dialog.$el.find('.linkCheckbox').click(); - // here fetch would be called and the server returns the expiration date - shareModel.get('linkShare').expiration = '2014-1-27 00:00:00'; - dialog.render(); - - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(true); - expect(dialog.$el.find('.datepicker').val()).toEqual('27-01-2014'); - - // disabling is not allowed - expect(dialog.$el.find('[name=expirationCheckbox]').prop('disabled')).toEqual(true); - dialog.$el.find('[name=expirationCheckbox]').click(); - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(true); - }); - it('enforces default date when enforced date setting is enabled and password is enforced', function() { - configModel.set({ - enforcePasswordForPublicLink: true, - isDefaultExpireDateEnabled: true, - isDefaultExpireDateEnforced: true - }); - dialog.render(); - dialog.$el.find('.linkCheckbox').click(); - // here fetch would be called and the server returns the expiration date - shareModel.get('linkShare').expiration = '2014-1-27 00:00:00'; - dialog.render(); - - //Enter password - dialog.$el.find('.linkPassText').val('foo'); - dialog.$el.find('.linkPassText').trigger(new $.Event('keyup', {keyCode: 13})); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({data: {token: 'xyz'}, status: 'success'}) - ); - - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(true); - expect(dialog.$el.find('.datepicker').val()).toEqual('27-01-2014'); - - // disabling is not allowed - expect(dialog.$el.find('[name=expirationCheckbox]').prop('disabled')).toEqual(true); - dialog.$el.find('[name=expirationCheckbox]').click(); - expect(dialog.$el.find('[name=expirationCheckbox]').prop('checked')).toEqual(true); - }); - it('sets picker minDate to today and no maxDate by default', function() { - dialog.render(); - dialog.$el.find('.linkCheckbox').click(); - dialog.$el.find('[name=expirationCheckbox]').click(); - expect($.datepicker._defaults.minDate).toEqual(expectedMinDate); - expect($.datepicker._defaults.maxDate).toEqual(null); - }); - it('limits the date range to X days after share time when enforced', function() { - configModel.set({ - isDefaultExpireDateEnabled: true, - isDefaultExpireDateEnforced: true - }); - dialog.render(); - dialog.$el.find('.linkCheckbox').click(); - expect($.datepicker._defaults.minDate).toEqual(expectedMinDate); - expect($.datepicker._defaults.maxDate).toEqual(new Date(2014, 0, 27, 0, 0, 0, 0)); - }); - it('limits the date range to X days after share time when enforced, even when redisplayed the next days', function() { - // item exists, was created two days ago - var shareItem = shareModel.get('linkShare'); - shareItem.expiration = '2014-1-27'; - // share time has time component but must be stripped later - shareItem.stime = new Date(2014, 0, 20, 11, 0, 25).getTime() / 1000; - configModel.set({ - isDefaultExpireDateEnabled: true, - isDefaultExpireDateEnforced: true - }); - dialog.render(); - expect($.datepicker._defaults.minDate).toEqual(expectedMinDate); - expect($.datepicker._defaults.maxDate).toEqual(new Date(2014, 0, 27, 0, 0, 0, 0)); - }); - }); - }); describe('check for avatar', function() { beforeEach(function() { @@ -455,8 +273,8 @@ describe('OC.Share.ShareDialogView', function() { it('test correct function calls', function() { expect(avatarStub.calledThrice).toEqual(true); expect(placeholderStub.callCount).toEqual(4); - expect(dialog.$('.shareWithList').children().length).toEqual(5); - expect(dialog.$('.avatar').length).toEqual(6); + expect(dialog.$('.shareWithList').children().length).toEqual(6); + expect(dialog.$('.avatar').length).toEqual(7); }); it('test avatar owner', function() { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 234aa418f56..a060131979d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -611,6 +611,7 @@ return array( 'OC\\Core\\Migrations\\Version14000Date20180518120534' => $baseDir . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\Core\\Migrations\\Version14000Date20180522074438' => $baseDir . '/core/Migrations/Version14000Date20180522074438.php', 'OC\\Core\\Migrations\\Version14000Date20180626223656' => $baseDir . '/core/Migrations/Version14000Date20180626223656.php', + 'OC\\Core\\Migrations\\Version14000Date20180712153140' => $baseDir . '/core/Migrations/Version14000Date20180712153140.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c54120586f9..4c6c55a59ad 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -641,6 +641,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version14000Date20180518120534' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\Core\\Migrations\\Version14000Date20180522074438' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180522074438.php', 'OC\\Core\\Migrations\\Version14000Date20180626223656' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180626223656.php', + 'OC\\Core\\Migrations\\Version14000Date20180712153140' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180712153140.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/private/Share/Share.php b/lib/private/Share/Share.php index 52e0628987b..0a9371d35b8 100644 --- a/lib/private/Share/Share.php +++ b/lib/private/Share/Share.php @@ -86,7 +86,6 @@ class Share extends Constants { ); if(count(self::$backendTypes) === 1) { Util::addScript('core', 'merged-share-backend'); - \OC_Util::addStyle('core', 'share'); } return true; } diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 5e52156d1d0..9c5d78a5958 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -30,8 +30,14 @@ namespace OC\Share20; use OC\Files\Cache\Cache; +use OCP\Defaults; use OCP\Files\Folder; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Mail\IMailer; use OCP\Share\IShare; +use OCP\Share\IShareHelper; use OCP\Share\IShareProvider; use OC\Share20\Exception\InvalidShare; use OC\Share20\Exception\ProviderException; @@ -67,6 +73,18 @@ class DefaultShareProvider implements IShareProvider { /** @var IRootFolder */ private $rootFolder; + /** @var IMailer */ + private $mailer; + + /** @var Defaults */ + private $defaults; + + /** @var IL10N */ + private $l; + + /** @var IURLGenerator */ + private $urlGenerator; + /** * DefaultShareProvider constructor. * @@ -74,16 +92,28 @@ class DefaultShareProvider implements IShareProvider { * @param IUserManager $userManager * @param IGroupManager $groupManager * @param IRootFolder $rootFolder + * @param IMailer $mailer ; + * @param Defaults $defaults + * @param IL10N $l + * @param IURLGenerator $urlGenerator */ public function __construct( IDBConnection $connection, IUserManager $userManager, IGroupManager $groupManager, - IRootFolder $rootFolder) { + IRootFolder $rootFolder, + IMailer $mailer, + Defaults $defaults, + IL10N $l, + IURLGenerator $urlGenerator) { $this->dbConn = $connection; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->rootFolder = $rootFolder; + $this->mailer = $mailer; + $this->defaults = $defaults; + $this->l = $l; + $this->urlGenerator = $urlGenerator; } /** @@ -197,6 +227,9 @@ class DefaultShareProvider implements IShareProvider { * @return \OCP\Share\IShare The share object */ public function update(\OCP\Share\IShare $share) { + + $originalShare = $this->getShareById($share->getId()); + if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { /* * We allow updating the recipient on user shares. @@ -211,6 +244,7 @@ class DefaultShareProvider implements IShareProvider { ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('note', $qb->createNamedParameter($share->getNote())) ->execute(); } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { $qb = $this->dbConn->getQueryBuilder(); @@ -222,6 +256,7 @@ class DefaultShareProvider implements IShareProvider { ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('note', $qb->createNamedParameter($share->getNote())) ->execute(); /* @@ -235,6 +270,7 @@ class DefaultShareProvider implements IShareProvider { ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('note', $qb->createNamedParameter($share->getNote())) ->execute(); /* @@ -259,9 +295,15 @@ class DefaultShareProvider implements IShareProvider { ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('token', $qb->createNamedParameter($share->getToken())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('note', $qb->createNamedParameter($share->getNote())) ->execute(); } + if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') { + $this->propagateNote($share); + } + + return $share; } @@ -875,6 +917,7 @@ class DefaultShareProvider implements IShareProvider { ->setShareType((int)$data['share_type']) ->setPermissions((int)$data['permissions']) ->setTarget($data['file_target']) + ->setNote($data['note']) ->setMailSend((bool)$data['mail_send']); $shareTime = new \DateTime(); @@ -1227,4 +1270,96 @@ class DefaultShareProvider implements IShareProvider { return $best; } + + /** + * propagate notes to the recipients + * + * @param IShare $share + * @throws \OCP\Files\NotFoundException + */ + private function propagateNote(IShare $share) { + if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { + $user = $this->userManager->get($share->getSharedWith()); + $this->sendNote([$user], $share); + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { + $group = $this->groupManager->get($share->getSharedWith()); + $groupMembers = $group->getUsers(); + $this->sendNote($groupMembers, $share); + } + } + + /** + * send note by mail + * + * @param array $recipients + * @param IShare $share + * @throws \OCP\Files\NotFoundException + */ + private function sendNote(array $recipients, IShare $share) { + + $toList = []; + + foreach ($recipients as $recipient) { + /** @var IUser $recipient */ + $email = $recipient->getEMailAddress(); + if ($email) { + $toList[$email] = $recipient->getDisplayName(); + } + } + + if (!empty($toList)) { + + $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('defaultShareProvider.sendNote'); + + $emailTemplate->setSubject($this->l->t('»%s« added a note to a file shared with you', [$initiatorDisplayName])); + $emailTemplate->addHeader(); + $emailTemplate->addHeading($htmlHeading, $plainHeading); + $emailTemplate->addBodyText(htmlspecialchars($note), $note); + + $link = $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $share->getNode()->getId()]); + $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(); + } + + if (count($toList) === 1) { + $message->setTo($toList); + } else { + $message->setTo([]); + $message->setBcc($toList); + } + $message->useTemplate($emailTemplate); + $this->mailer->send($message); + } + + } } diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index e4d34146911..0aacca409d1 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -81,7 +81,11 @@ class ProviderFactory implements IProviderFactory { $this->serverContainer->getDatabaseConnection(), $this->serverContainer->getUserManager(), $this->serverContainer->getGroupManager(), - $this->serverContainer->getLazyRootFolder() + $this->serverContainer->getLazyRootFolder(), + $this->serverContainer->getMailer(), + $this->serverContainer->query(Defaults::class), + $this->serverContainer->getL10N('sharing'), + $this->serverContainer->getURLGenerator() ); } diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index d7810165dac..e54497c9b55 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -57,6 +57,8 @@ class Share implements \OCP\Share\IShare { private $shareOwner; /** @var int */ private $permissions; + /** @var string */ + private $note = ''; /** @var \DateTime */ private $expireDate; /** @var string */ @@ -311,6 +313,24 @@ class Share implements \OCP\Share\IShare { /** * @inheritdoc */ + public function setNote($note) { + $this->note = $note; + return $this; + } + + /** + * @inheritdoc + */ + public function getNote() { + if (is_string($this->note)) { + return $this->note; + } + return ''; + } + + /** + * @inheritdoc + */ public function setExpirationDate($expireDate) { //TODO checks diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index 1efdf8b441c..fc2e5798e1c 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -42,6 +42,7 @@ use OC\Template\JSCombiner; use OC\Template\JSConfigHelper; use OC\Template\SCSSCacher; use OCP\Defaults; +use OC\AppFramework\Http\Request; class TemplateLayout extends \OC_Template { @@ -61,6 +62,9 @@ class TemplateLayout extends \OC_Template { // yes - should be injected .... $this->config = \OC::$server->getConfig(); + if(\OCP\Util::isIE()) { + \OC_Util::addStyle('ie'); + } // Decide which page we show if($renderAs == 'user') { diff --git a/lib/private/legacy/util.php b/lib/private/legacy/util.php index b285eb382e7..a581397212a 100644 --- a/lib/private/legacy/util.php +++ b/lib/private/legacy/util.php @@ -64,6 +64,7 @@ use OCP\IConfig; use OCP\IGroupManager; use OCP\ILogger; use OCP\IUser; +use OC\AppFramework\Http\Request; class OC_Util { public static $scripts = array(); @@ -1528,4 +1529,13 @@ class OC_Util { } } + /** + * is this Internet explorer ? + * + * @return boolean + */ + public static function isIe() { + return preg_match(Request::USER_AGENT_IE, $_SERVER['HTTP_USER_AGENT']) === 1; + } + } diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php index 870794d6536..5303cde45a6 100644 --- a/lib/public/Share/IShare.php +++ b/lib/public/Share/IShare.php @@ -223,6 +223,24 @@ interface IShare { public function getPermissions(); /** + * Attach a note to a share + * + * @param string $note + * @return \OCP\Share\IShare The modified object + * @since 14.0.0 + */ + public function setNote($note); + + /** + * Get note attached to a share + * + * @return string + * @since 14.0.0 + */ + public function getNote(); + + + /** * Set the expiration date * * @param null|\DateTime $expireDate diff --git a/lib/public/Util.php b/lib/public/Util.php index 27b703ea2d8..9892f4a1ecb 100644 --- a/lib/public/Util.php +++ b/lib/public/Util.php @@ -558,4 +558,14 @@ class Util { } return self::$needUpgradeCache; } + + /** + * is this Internet explorer ? + * + * @return boolean + * @since 14.0.0 + */ + public static function isIe() { + return \OC_Util::isIe(); + } } diff --git a/tests/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php index 53d365b1b09..ab9fc8fe29c 100644 --- a/tests/acceptance/features/bootstrap/FilesAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppContext.php @@ -187,21 +187,49 @@ class FilesAppContext implements Context, ActorAwareInterface { /** * @return Locator */ - public static function shareLinkCheckbox() { - // forThe()->checkbox("Share link") can not be used here; that would - // return the checkbox itself, but the element that the user interacts - // with is the label. - return Locator::forThe()->xpath("//label[normalize-space() = 'Share link']")-> + public static function shareLinkRow() { + return Locator::forThe()->id("shareLink")-> descendantOf(self::detailsView())-> + describedAs("Share link row in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function shareLinkCheckbox() { + // forThe()->checkbox("Enable") can not be used here; that would return + // the checkbox itself, but the element that the user interacts with is + // the label. + return Locator::forThe()->xpath("//label[normalize-space() = 'Enable']")-> + descendantOf(self::shareLinkRow())-> describedAs("Share link checkbox in the details view in Files app"); } /** * @return Locator */ - public static function shareLinkField() { - return Locator::forThe()->css(".linkText")->descendantOf(self::detailsView())-> - describedAs("Share link field in the details view in Files app"); + public static function shareLinkMenuButton() { + return Locator::forThe()->css(".share-menu > .icon")-> + descendantOf(self::shareLinkRow())-> + describedAs("Share link menu button in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function shareLinkMenu() { + return Locator::forThe()->css(".share-menu > .menu")-> + descendantOf(self::shareLinkRow())-> + describedAs("Share link menu in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function copyUrlMenuItem() { + return Locator::forThe()->xpath("//a[normalize-space() = 'Copy URL']")-> + descendantOf(self::shareLinkMenu())-> + describedAs("Copy URL menu item in the share link menu in the details view in Files app"); } /** @@ -212,7 +240,7 @@ class FilesAppContext implements Context, ActorAwareInterface { // that would return the radio button itself, but the element that the // user interacts with is the label. return Locator::forThe()->xpath("//label[normalize-space() = 'Allow upload and editing']")-> - descendantOf(self::detailsView())-> + descendantOf(self::shareLinkMenu())-> describedAs("Allow upload and editing radio button in the details view in Files app"); } @@ -224,7 +252,7 @@ class FilesAppContext implements Context, ActorAwareInterface { // would return the checkbox itself, but the element that the user // interacts with is the label. return Locator::forThe()->xpath("//label[normalize-space() = 'Password protect']")-> - descendantOf(self::detailsView())-> + descendantOf(self::shareLinkMenu())-> describedAs("Password protect checkbox in the details view in Files app"); } @@ -232,7 +260,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function passwordProtectField() { - return Locator::forThe()->css(".linkPassText")->descendantOf(self::detailsView())-> + return Locator::forThe()->css(".linkPassText")->descendantOf(self::shareLinkMenu())-> describedAs("Password protect field in the details view in Files app"); } @@ -240,7 +268,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function passwordProtectWorkingIcon() { - return Locator::forThe()->css(".linkPass .icon-loading-small")->descendantOf(self::detailsView())-> + return Locator::forThe()->css(".linkPassMenu .icon-loading-small")->descendantOf(self::shareLinkMenu())-> describedAs("Password protect working icon in the details view in Files app"); } @@ -278,17 +306,14 @@ class FilesAppContext implements Context, ActorAwareInterface { * @Given I write down the shared link */ public function iWriteDownTheSharedLink() { - // The shared link field always exists in the DOM (once the "Sharing" - // tab is loaded), but its value is the actual shared link only when it - // is visible. - if (!WaitFor::elementToBeEventuallyShown( - $this->actor, - self::shareLinkField(), - $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { - PHPUnit_Framework_Assert::fail("The shared link was not shown yet after $timeout seconds"); - } + $this->showShareLinkMenuIfNeeded(); - $this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::shareLinkField())->getValue(); + $this->actor->find(self::copyUrlMenuItem(), 2)->click(); + + // Clicking on the menu item copies the link to the clipboard, but it is + // not possible to access that value from the acceptance tests. Due to + // this the value of the attribute that holds the URL is used instead. + $this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::copyUrlMenuItem(), 2)->getWrappedElement()->getAttribute("data-clipboard-text"); } /** @@ -313,14 +338,18 @@ class FilesAppContext implements Context, ActorAwareInterface { * @When I set the shared link as editable */ public function iSetTheSharedLinkAsEditable() { - $this->actor->find(self::allowUploadAndEditingRadioButton(), 10)->click(); + $this->showShareLinkMenuIfNeeded(); + + $this->actor->find(self::allowUploadAndEditingRadioButton(), 2)->click(); } /** * @When I protect the shared link with the password :password */ public function iProtectTheSharedLinkWithThePassword($password) { - $this->actor->find(self::passwordProtectCheckbox(), 10)->click(); + $this->showShareLinkMenuIfNeeded(); + + $this->actor->find(self::passwordProtectCheckbox(), 2)->click(); $this->actor->find(self::passwordProtectField(), 2)->setValue($password . "\r"); } @@ -460,4 +489,17 @@ class FilesAppContext implements Context, ActorAwareInterface { $this->iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown(); } + private function showShareLinkMenuIfNeeded() { + // In some cases the share menu is hidden after clicking on an action of + // the menu. Therefore, if the menu is visible, wait a little just in + // case it is in the process of being hidden due to a previous action, + // in which case it is shown again. + if (WaitFor::elementToBeEventuallyNotShown( + $this->actor, + self::shareLinkMenu(), + $timeout = 2 * $this->actor->getFindTimeoutMultiplier())) { + $this->actor->find(self::shareLinkMenuButton(), 10)->click(); + } + } + } diff --git a/tests/lib/Share20/DefaultShareProviderTest.php b/tests/lib/Share20/DefaultShareProviderTest.php index 230c8db40ce..19f37160627 100644 --- a/tests/lib/Share20/DefaultShareProviderTest.php +++ b/tests/lib/Share20/DefaultShareProviderTest.php @@ -22,15 +22,19 @@ namespace Test\Share20; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Defaults; use OCP\Files\File; use OCP\Files\Folder; use OCP\IDBConnection; use OCP\IGroup; +use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\IGroupManager; use OCP\Files\IRootFolder; use OC\Share20\DefaultShareProvider; +use OCP\Mail\IMailer; use OCP\Share\IShare; /** @@ -56,11 +60,27 @@ class DefaultShareProviderTest extends \Test\TestCase { /** @var DefaultShareProvider */ protected $provider; + /** @var \PHPUnit_Framework_MockObject_MockObject|IMailer */ + protected $mailer; + + /** @var \PHPUnit_Framework_MockObject_MockObject|IL10N */ + protected $l10n; + + /** @var \PHPUnit_Framework_MockObject_MockObject|Defaults */ + protected $defaults; + + /** @var \PHPUnit_Framework_MockObject_MockObject|IURLGenerator */ + protected $urlGenerator; + public function setUp() { $this->dbConn = \OC::$server->getDatabaseConnection(); $this->userManager = $this->createMock(IUserManager::class); $this->groupManager = $this->createMock(IGroupManager::class); $this->rootFolder = $this->createMock(IRootFolder::class); + $this->mailer = $this->createMock(IMailer::class); + $this->l10n = $this->createMock(IL10N::class); + $this->defaults = $this->getMockBuilder(Defaults::class)->disableOriginalConstructor()->getMock(); + $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->userManager->expects($this->any())->method('userExists')->willReturn(true); @@ -71,7 +91,11 @@ class DefaultShareProviderTest extends \Test\TestCase { $this->dbConn, $this->userManager, $this->groupManager, - $this->rootFolder + $this->rootFolder, + $this->mailer, + $this->defaults, + $this->l10n, + $this->urlGenerator ); } @@ -403,6 +427,10 @@ class DefaultShareProviderTest extends \Test\TestCase { $this->userManager, $this->groupManager, $this->rootFolder, + $this->mailer, + $this->defaults, + $this->l10n, + $this->urlGenerator ]) ->setMethods(['getShareById']) ->getMock(); @@ -493,6 +521,10 @@ class DefaultShareProviderTest extends \Test\TestCase { $this->userManager, $this->groupManager, $this->rootFolder, + $this->mailer, + $this->defaults, + $this->l10n, + $this->urlGenerator ]) ->setMethods(['getShareById']) ->getMock(); @@ -2368,7 +2400,11 @@ class DefaultShareProviderTest extends \Test\TestCase { $this->dbConn, $userManager, $groupManager, - $rootFolder + $rootFolder, + $this->mailer, + $this->defaults, + $this->l10n, + $this->urlGenerator ); $password = md5(time()); @@ -2461,7 +2497,11 @@ class DefaultShareProviderTest extends \Test\TestCase { $this->dbConn, $userManager, $groupManager, - $rootFolder + $rootFolder, + $this->mailer, + $this->defaults, + $this->l10n, + $this->urlGenerator ); $u1 = $userManager->createUser('testShare1', 'test'); @@ -2552,7 +2592,11 @@ class DefaultShareProviderTest extends \Test\TestCase { $this->dbConn, $userManager, $groupManager, - $rootFolder + $rootFolder, + $this->mailer, + $this->defaults, + $this->l10n, + $this->urlGenerator ); $u1 = $userManager->createUser('testShare1', 'test'); diff --git a/version.php b/version.php index 5c288428c13..c18ca01286b 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(14, 0, 0, 10); +$OC_Version = array(14, 0, 0, 11); // The human readable string $OC_VersionString = '14.0.0 alpha'; |