From ea6f423e2c8e50cf1357a0e2182dc4c9a9bf981e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Thu, 23 May 2019 17:03:04 +0200 Subject: [PATCH 01/14] Extend data returned when searching remote shares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .../sharees_features/sharees.feature | 2 +- .../sharees_provisioningapiv2.feature | 2 +- .../Collaborators/RemoteGroupPlugin.php | 22 ++++++++++++++++++- .../Collaborators/RemotePlugin.php | 5 ++++- .../Collaborators/RemotePluginTest.php | 10 ++++----- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/build/integration/sharees_features/sharees.feature b/build/integration/sharees_features/sharees.feature index 58570cfc5f..5a6291d1e2 100644 --- a/build/integration/sharees_features/sharees.feature +++ b/build/integration/sharees_features/sharees.feature @@ -206,7 +206,7 @@ Feature: sharees Then "exact groups" sharees returned is empty Then "groups" sharees returned is empty Then "exact remotes" sharees returned are - | test@localhost | 6 | test@localhost | + | test (localhost) | 6 | test@localhost | Then "remotes" sharees returned is empty Scenario: Remote sharee for calendars not allowed diff --git a/build/integration/sharees_features/sharees_provisioningapiv2.feature b/build/integration/sharees_features/sharees_provisioningapiv2.feature index 37ab896ee2..6f2b8df8e0 100644 --- a/build/integration/sharees_features/sharees_provisioningapiv2.feature +++ b/build/integration/sharees_features/sharees_provisioningapiv2.feature @@ -206,7 +206,7 @@ Feature: sharees_provisioningapiv2 Then "exact groups" sharees returned is empty Then "groups" sharees returned is empty Then "exact remotes" sharees returned are - | test@localhost | 6 | test@localhost | + | test (localhost) | 6 | test@localhost | Then "remotes" sharees returned is empty Scenario: Remote sharee for calendars not allowed diff --git a/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php b/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php index 6e0979fe41..d9e1f2fd49 100644 --- a/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php +++ b/lib/private/Collaboration/Collaborators/RemoteGroupPlugin.php @@ -57,11 +57,15 @@ class RemoteGroupPlugin implements ISearchPlugin { $resultType = new SearchResultType('remote_groups'); if ($this->enabled && $this->cloudIdManager->isValidCloudId($search) && $offset === 0) { + list($remoteGroup, $serverUrl) = $this->splitGroupRemote($search); $result['exact'][] = [ - 'label' => $search, + 'label' => $remoteGroup . " ($serverUrl)", + 'guid' => $remoteGroup, + 'name' => $remoteGroup, 'value' => [ 'shareType' => Share::SHARE_TYPE_REMOTE_GROUP, 'shareWith' => $search, + 'server' => $serverUrl, ], ]; } @@ -71,4 +75,20 @@ class RemoteGroupPlugin implements ISearchPlugin { return true; } + /** + * split group and remote from federated cloud id + * + * @param string $address federated share address + * @return array [user, remoteURL] + * @throws \InvalidArgumentException + */ + public function splitGroupRemote($address) { + try { + $cloudId = $this->cloudIdManager->resolveCloudId($address); + return [$cloudId->getUser(), $cloudId->getRemote()]; + } catch (\InvalidArgumentException $e) { + throw new \InvalidArgumentException('Invalid Federated Cloud ID', 0, $e); + } + } + } diff --git a/lib/private/Collaboration/Collaborators/RemotePlugin.php b/lib/private/Collaboration/Collaborators/RemotePlugin.php index d877346b15..fd14e7e03b 100644 --- a/lib/private/Collaboration/Collaborators/RemotePlugin.php +++ b/lib/private/Collaboration/Collaborators/RemotePlugin.php @@ -152,10 +152,13 @@ class RemotePlugin implements ISearchPlugin { $localUser = $this->userManager->get($remoteUser); if ($localUser === null || $search !== $localUser->getCloudId()) { $result['exact'][] = [ - 'label' => $search, + 'label' => $remoteUser . " ($serverUrl)", + 'uuid' => $remoteUser, + 'name' => $remoteUser, 'value' => [ 'shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => $search, + 'server' => $serverUrl, ], ]; } diff --git a/tests/lib/Collaboration/Collaborators/RemotePluginTest.php b/tests/lib/Collaboration/Collaborators/RemotePluginTest.php index aff6818576..560e72a984 100644 --- a/tests/lib/Collaboration/Collaborators/RemotePluginTest.php +++ b/tests/lib/Collaboration/Collaborators/RemotePluginTest.php @@ -152,7 +152,7 @@ class RemotePluginTest extends TestCase { 'test@remote', [], true, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]], + ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]], false, true, ], @@ -160,7 +160,7 @@ class RemotePluginTest extends TestCase { 'test@remote', [], false, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]], + ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]], false, true, ], @@ -238,7 +238,7 @@ class RemotePluginTest extends TestCase { ], ], true, - ['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid', 'type' => '', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]], + ['remotes' => [['name' => 'User @ Localhost', 'label' => 'User @ Localhost (username@localhost)', 'uuid' => 'uid', 'type' => '', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]], false, true, ], @@ -264,7 +264,7 @@ class RemotePluginTest extends TestCase { ], ], false, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]]]], + ['remotes' => [], 'exact' => ['remotes' => [['label' => 'test (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote', 'server' => 'remote'], 'uuid' => 'test', 'name' => 'test']]]], false, true, ], @@ -370,7 +370,7 @@ class RemotePluginTest extends TestCase { ], ], false, - ['remotes' => [], 'exact' => ['remotes' => [['label' => 'user space@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote']]]]], + ['remotes' => [], 'exact' => ['remotes' => [['label' => 'user space (remote)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote', 'server' => 'remote'], 'uuid' => 'user space', 'name' => 'user space']]]], false, true, ], From fd90af50d910e659aa8df0d380424383c6c09620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Thu, 23 May 2019 17:03:04 +0200 Subject: [PATCH 02/14] Add OCA.Files.Sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .babelrc.js | 5 +- Makefile | 2 + apps/comments/src/filesplugin.js | 2 +- apps/files/css/files.scss | 3 +- apps/files/js/fileactions.js | 6 + apps/files/js/filelist.js | 69 +- apps/files/js/merged-index.json | 47 +- apps/files/src/components/LegacyTab.vue | 89 ++ apps/files/src/components/LegacyView.vue | 59 ++ apps/files/src/models/Tab.js | 59 ++ apps/files/src/services/FileInfo.js | 67 ++ apps/files/src/services/Sidebar.js | 109 +++ apps/files/src/sidebar.js | 59 ++ apps/files/src/views/Sidebar.vue | 345 ++++++++ apps/files/webpack.js | 13 + apps/files_sharing/appinfo/app.php | 1 + apps/files_sharing/css/icons.scss | 32 + apps/files_sharing/list.php | 1 + .../src/components/SharingEntry.vue | 249 ++++++ .../src/components/SharingEntryInternal.vue | 117 +++ .../src/components/SharingEntryLink.vue | 769 ++++++++++++++++++ .../src/components/SharingEntrySimple.vue | 97 +++ .../src/components/SharingInput.vue | 444 ++++++++++ apps/files_sharing/src/files_sharing_tab.js | 39 + .../files_sharing/src/mixins/ShareRequests.js | 114 +++ apps/files_sharing/src/mixins/ShareTypes.js | 39 + apps/files_sharing/src/mixins/SharesMixin.js | 303 +++++++ apps/files_sharing/src/models/Share.js | 444 ++++++++++ .../src/services/ConfigService.js | 223 +++++ .../src/services/ExternalLinkActions.js | 63 ++ .../files_sharing/src/services/ShareSearch.js | 71 ++ apps/files_sharing/src/share.js | 58 +- apps/files_sharing/src/sharebreadcrumbview.js | 2 +- apps/files_sharing/src/utils/SharedWithMe.js | 86 ++ .../src/views/SharingLinkList.vue | 141 ++++ apps/files_sharing/src/views/SharingList.vue | 76 ++ apps/files_sharing/src/views/SharingTab.vue | 318 ++++++++ apps/files_sharing/webpack.js | 1 + core/js/files/client.js | 7 + core/src/Polyfill/closest.js | 19 + core/src/Polyfill/index.js | 3 +- core/src/main.js | 4 +- core/src/views/Login.vue | 2 +- package-lock.json | 40 +- package.json | 10 +- webpack.common.js | 9 +- 46 files changed, 4607 insertions(+), 109 deletions(-) create mode 100644 apps/files/src/components/LegacyTab.vue create mode 100644 apps/files/src/components/LegacyView.vue create mode 100644 apps/files/src/models/Tab.js create mode 100644 apps/files/src/services/FileInfo.js create mode 100644 apps/files/src/services/Sidebar.js create mode 100644 apps/files/src/sidebar.js create mode 100644 apps/files/src/views/Sidebar.vue create mode 100644 apps/files/webpack.js create mode 100644 apps/files_sharing/css/icons.scss create mode 100644 apps/files_sharing/src/components/SharingEntry.vue create mode 100644 apps/files_sharing/src/components/SharingEntryInternal.vue create mode 100644 apps/files_sharing/src/components/SharingEntryLink.vue create mode 100644 apps/files_sharing/src/components/SharingEntrySimple.vue create mode 100644 apps/files_sharing/src/components/SharingInput.vue create mode 100644 apps/files_sharing/src/files_sharing_tab.js create mode 100644 apps/files_sharing/src/mixins/ShareRequests.js create mode 100644 apps/files_sharing/src/mixins/ShareTypes.js create mode 100644 apps/files_sharing/src/mixins/SharesMixin.js create mode 100644 apps/files_sharing/src/models/Share.js create mode 100644 apps/files_sharing/src/services/ConfigService.js create mode 100644 apps/files_sharing/src/services/ExternalLinkActions.js create mode 100644 apps/files_sharing/src/services/ShareSearch.js create mode 100644 apps/files_sharing/src/utils/SharedWithMe.js create mode 100644 apps/files_sharing/src/views/SharingLinkList.vue create mode 100644 apps/files_sharing/src/views/SharingList.vue create mode 100644 apps/files_sharing/src/views/SharingTab.vue create mode 100644 core/src/Polyfill/closest.js diff --git a/.babelrc.js b/.babelrc.js index 5cfbddd7a0..004c14b511 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,5 +1,8 @@ module.exports = { - plugins: ['@babel/plugin-syntax-dynamic-import'], + plugins: [ + '@babel/plugin-syntax-dynamic-import', + ['@babel/plugin-proposal-class-properties', { loose: true }] + ], presets: [ [ '@babel/preset-env', diff --git a/Makefile b/Makefile index a3e1d4eab6..0de4d002ee 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ lint-fix-watch: clean: rm -rf apps/accessibility/js/ rm -rf apps/comments/js/ + rm -rf apps/files/js/dist/ rm -rf apps/files_sharing/js/dist/ rm -rf apps/files_trashbin/js/ rm -rf apps/files_versions/js/ @@ -47,6 +48,7 @@ clean-dev: clean-git: clean git checkout -- apps/accessibility/js/ git checkout -- apps/comments/js/ + git checkout -- apps/files/js/dist/ git checkout -- apps/files_sharing/js/dist/ git checkout -- apps/files_trashbin/js/ git checkout -- apps/files_versions/js/ diff --git a/apps/comments/src/filesplugin.js b/apps/comments/src/filesplugin.js index e315dd2fef..3e0cdd7f70 100644 --- a/apps/comments/src/filesplugin.js +++ b/apps/comments/src/filesplugin.js @@ -104,7 +104,7 @@ actionHandler: function(fileName, context) { context.$file.find('.action-comment').tooltip('hide') // open sidebar in comments section - context.fileList.showDetailsView(fileName, 'commentsTabView') + context.fileList.showDetailsView(fileName, 'comments') } }) diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 54f83f25be..9c1869d1ff 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -85,8 +85,9 @@ } /* fit app list view heights */ -.app-files #app-content>.viewcontainer { +.app-files #app-content > .viewcontainer { min-height: 0%; + width: 100%; } .app-files #app-content { diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index d800f2b8eb..571cdcf6c3 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -704,6 +704,12 @@ } context.fileList.do_delete(fileName, context.dir); $('.tipsy').remove(); + + // close sidebar on delete + const path = context.dir + '/' + fileName + if (OCA.Files.Sidebar && OCA.Files.Sidebar.file === path) { + OCA.Files.Sidebar.file = undefined + } } }); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 58e2bfae7f..8cca43d574 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -610,11 +610,11 @@ * @param {string} [tabId] optional tab id to select */ showDetailsView: function(fileName, tabId) { + console.warn('showDetailsView is deprecated! Use OCA.Files.Sidebar.activeTab. It will be removed in nextcloud 20.'); this._updateDetailsView(fileName); if (tabId) { - this._detailsView.selectTab(tabId); + OCA.Files.Sidebar.activeTab = tabId; } - OC.Apps.showAppSidebar(this._detailsView.$el); }, /** @@ -623,48 +623,23 @@ * @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object * @param {boolean} [show=true] whether to open the sidebar if it was closed */ - _updateDetailsView: function(fileName, show) { - if (!this._detailsView) { + _updateDetailsView: function(fileName) { + if (!(OCA.Files && OCA.Files.Sidebar)) { + console.error('No sidebar available'); return; } - // show defaults to true - show = _.isUndefined(show) || !!show; - var oldFileInfo = this._detailsView.getFileInfo(); - if (oldFileInfo) { - // TODO: use more efficient way, maybe track the highlight - this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted'); - oldFileInfo.off('change', this._onSelectedModelChanged, this); - } - if (!fileName) { - this._detailsView.setFileInfo(null); - if (this._currentFileModel) { - this._currentFileModel.off(); - } - this._currentFileModel = null; - OC.Apps.hideAppSidebar(this._detailsView.$el); - return; + OCA.Files.Sidebar.file = null + return + } else if (typeof fileName !== 'string') { + fileName = '' } - if (show && this._detailsView.$el.hasClass('disappear')) { - OC.Apps.showAppSidebar(this._detailsView.$el); - } - - if (fileName instanceof OCA.Files.FileInfoModel) { - var model = fileName; - } else { - var $tr = this.findFileEl(fileName); - var model = this.getModelForFile($tr); - $tr.addClass('highlighted'); - } - - this._currentFileModel = model; - - this._replaceDetailsViewElementIfNeeded(); - - this._detailsView.setFileInfo(model); - this._detailsView.$el.scrollTop(0); + // open sidebar and set file + const dir = `${this.dirInfo.path}/${this.dirInfo.name}` + const path = `${dir}/${fileName}` + OCA.Files.Sidebar.file = path.replace('//', '/') }, /** @@ -1404,6 +1379,13 @@ return OC.MimeType.getIconUrl('dir-external'); } else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') { return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType); + } else if (fileInfo.shareTypes && ( + fileInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) > -1 + || fileInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_EMAIL) > -1) + ) { + return OC.MimeType.getIconUrl('dir-public') + } else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) { + return OC.MimeType.getIconUrl('dir-shared') } return OC.MimeType.getIconUrl('dir'); } @@ -3654,8 +3636,10 @@ * Register a tab view to be added to all views */ registerTabView: function(tabView) { - if (this._detailsView) { - this._detailsView.addTabView(tabView); + console.warn('registerTabView is deprecated! It will be removed in nextcloud 20.'); + const name = tabView.getLabel() + if (name) { + OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab(name, tabView, true)) } }, @@ -3663,8 +3647,9 @@ * Register a detail view to be added to all views */ registerDetailView: function(detailView) { - if (this._detailsView) { - this._detailsView.addDetailView(detailView); + console.warn('registerDetailView is deprecated! It will be removed in nextcloud 20.'); + if (detailView.el) { + OCA.Files.Sidebar.registerSecondaryView(detailView) } }, diff --git a/apps/files/js/merged-index.json b/apps/files/js/merged-index.json index 8d25daa6b3..b673da858c 100644 --- a/apps/files/js/merged-index.json +++ b/apps/files/js/merged-index.json @@ -1,33 +1,34 @@ [ + "dist/sidebar.js", "app.js", - "templates.js", - "file-upload.js", - "newfilemenu.js", - "jquery.fileupload.js", - "jquery-visibility.js", - "fileinfomodel.js", - "filesummary.js", - "filemultiselectmenu.js", "breadcrumb.js", - "filelist.js", - "search.js", - "favoritesfilelist.js", - "recentfilelist.js", - "tagsplugin.js", - "gotoplugin.js", - "favoritesplugin.js", - "recentplugin.js", "detailfileinfoview.js", - "sidebarpreviewmanager.js", - "sidebarpreviewtext.js", - "detailtabview.js", - "semaphore.js", - "mainfileinfodetailview.js", - "operationprogressbar.js", "detailsview.js", + "detailtabview.js", + "favoritesfilelist.js", + "favoritesplugin.js", + "file-upload.js", "fileactions.js", "fileactionsmenu.js", + "fileinfomodel.js", + "filelist.js", + "filemultiselectmenu.js", "files.js", + "filesummary.js", + "gotoplugin.js", + "jquery-visibility.js", + "jquery.fileupload.js", "keyboardshortcuts.js", - "navigation.js" + "mainfileinfodetailview.js", + "navigation.js", + "newfilemenu.js", + "operationprogressbar.js", + "recentfilelist.js", + "recentplugin.js", + "search.js", + "semaphore.js", + "sidebarpreviewmanager.js", + "sidebarpreviewtext.js", + "tagsplugin.js", + "templates.js" ] diff --git a/apps/files/src/components/LegacyTab.vue b/apps/files/src/components/LegacyTab.vue new file mode 100644 index 0000000000..9a85ee7f07 --- /dev/null +++ b/apps/files/src/components/LegacyTab.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/apps/files/src/components/LegacyView.vue b/apps/files/src/components/LegacyView.vue new file mode 100644 index 0000000000..e4a07ac3e5 --- /dev/null +++ b/apps/files/src/components/LegacyView.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/apps/files/src/models/Tab.js b/apps/files/src/models/Tab.js new file mode 100644 index 0000000000..28902b0e75 --- /dev/null +++ b/apps/files/src/models/Tab.js @@ -0,0 +1,59 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default class Tab { + + #component; + #legacy; + #name; + + /** + * Create a new tab instance + * + * @param {string} name the name of this tab + * @param {Object} component the vue component + * @param {boolean} [legacy] is this a legacy tab + */ + constructor(name, component, legacy) { + this.#name = name + this.#component = component + this.#legacy = legacy === true + + if (this.#legacy) { + console.warn('Legacy tabs are deprecated! They will be removed in nextcloud 20.') + } + + } + + get name() { + return this.#name + } + + get component() { + return this.#component + } + + get isLegacyTab() { + return this.#legacy === true + } + +} diff --git a/apps/files/src/services/FileInfo.js b/apps/files/src/services/FileInfo.js new file mode 100644 index 0000000000..aa026df144 --- /dev/null +++ b/apps/files/src/services/FileInfo.js @@ -0,0 +1,67 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import axios from '@nextcloud/axios' + +export default async function(url) { + const response = await axios({ + method: 'PROPFIND', + url, + data: ` + + + + + + + + + + + + + + + + + + + + + + ` + }) + + // TODO: create new parser or use cdav-lib when available + const file = OCA.Files.App.fileList.filesClient._client.parseMultiStatus(response.data) + // TODO: create new parser or use cdav-lib when available + const fileInfo = OCA.Files.App.fileList.filesClient._parseFileInfo(file[0]) + + // TODO remove when no more legacy backbone is used + fileInfo.get = (key) => fileInfo[key] + fileInfo.isDirectory = () => fileInfo.mimetype === 'httpd/unix-directory' + + return fileInfo +} diff --git a/apps/files/src/services/Sidebar.js b/apps/files/src/services/Sidebar.js new file mode 100644 index 0000000000..8f02a1b51a --- /dev/null +++ b/apps/files/src/services/Sidebar.js @@ -0,0 +1,109 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default class Sidebar { + + #state; + #view; + + constructor() { + // init empty state + this.#state = {} + + // init default values + this.#state.tabs = [] + this.#state.views = [] + this.#state.file = '' + this.#state.activeTab = '' + console.debug('OCA.Files.Sidebar initialized') + } + + /** + * Get the sidebar state + * + * @readonly + * @memberof Sidebar + * @returns {Object} the data state + */ + get state() { + return this.#state + } + + /** + * @memberof Sidebar + * Register a new tab view + * + * @param {Object} tab a new unregistered tab + * @memberof Sidebar + * @returns {Boolean} + */ + registerTab(tab) { + const hasDuplicate = this.#state.tabs.findIndex(check => check.name === tab.name) > -1 + if (!hasDuplicate) { + this.#state.tabs.push(tab) + return true + } + console.error(`An tab with the same name ${tab.name} already exists`, tab) + return false + } + + registerSecondaryView(view) { + const hasDuplicate = this.#state.views.findIndex(check => check.cid === view.cid) > -1 + if (!hasDuplicate) { + this.#state.views.push(view) + return true + } + console.error(`A similar view already exists`, view) + return false + } + + /** + * Set the current sidebar file data + * + * @param {string} path the file path to load + * @memberof Sidebar + */ + set file(path) { + this.#state.file = path + } + + /** + * Set the current sidebar file data + * + * @returns {String} the current opened file + * @memberof Sidebar + */ + get file() { + return this.#state.file + } + + /** + * Set the current sidebar tab + * + * @param {string} id the tab unique id + * @memberof Sidebar + */ + set activeTab(id) { + this.#state.activeTab = id + } + +} diff --git a/apps/files/src/sidebar.js b/apps/files/src/sidebar.js new file mode 100644 index 0000000000..b508e8aee2 --- /dev/null +++ b/apps/files/src/sidebar.js @@ -0,0 +1,59 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import Vue from 'vue' +import SidebarView from './views/Sidebar.vue' +import Sidebar from './services/Sidebar' +import Tab from './models/Tab' +import VueClipboard from 'vue-clipboard2' + +Vue.use(VueClipboard) + +Vue.prototype.t = t + +window.addEventListener('DOMContentLoaded', () => { + // Init Sidebar Service + if (window.OCA && window.OCA.Files) { + Object.assign(window.OCA.Files, { Sidebar: new Sidebar() }) + Object.assign(window.OCA.Files.Sidebar, { Tab }) + } + + // Make sure we have a proper layout + if (document.getElementById('content')) { + + // Make sure we have a mountpoint + if (!document.getElementById('app-sidebar')) { + var contentElement = document.getElementById('content') + var sidebarElement = document.createElement('div') + sidebarElement.id = 'app-sidebar' + contentElement.appendChild(sidebarElement) + } + } + + // Init vue app + const AppSidebar = new Vue({ + // eslint-disable-next-line vue/match-component-file-name + name: 'SidebarRoot', + render: h => h(SidebarView) + }) + AppSidebar.$mount('#app-sidebar') +}) diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue new file mode 100644 index 0000000000..9a00df1737 --- /dev/null +++ b/apps/files/src/views/Sidebar.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/apps/files/webpack.js b/apps/files/webpack.js new file mode 100644 index 0000000000..4007722031 --- /dev/null +++ b/apps/files/webpack.js @@ -0,0 +1,13 @@ +const path = require('path'); + +module.exports = { + entry: { + 'sidebar': path.join(__dirname, 'src', 'sidebar.js'), + }, + output: { + path: path.resolve(__dirname, './js/dist/'), + publicPath: '/js/', + filename: '[name].js', + chunkFilename: 'files.[id].js' + } +} diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php index 747c202074..32159f7b97 100644 --- a/apps/files_sharing/appinfo/app.php +++ b/apps/files_sharing/appinfo/app.php @@ -43,6 +43,7 @@ $eventDispatcher->addListener( 'OCA\Files::loadAdditionalScripts', function() { \OCP\Util::addScript('files_sharing', 'dist/additionalScripts'); + \OCP\Util::addStyle('files_sharing', 'icons'); } ); \OC::$server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () { diff --git a/apps/files_sharing/css/icons.scss b/apps/files_sharing/css/icons.scss new file mode 100644 index 0000000000..002235b6e3 --- /dev/null +++ b/apps/files_sharing/css/icons.scss @@ -0,0 +1,32 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +// This is the icons used in the sharing ui (multiselect) +.icon-room { + @include icon-color('app', 'spreed', $color-black); +} +.icon-circle { + @include icon-color('circles', 'circles', $color-black, 3, false); +} +.icon-guests { + @include icon-color('app', 'guests', $color-black); +} \ No newline at end of file diff --git a/apps/files_sharing/list.php b/apps/files_sharing/list.php index 219fe2863e..5517c39971 100644 --- a/apps/files_sharing/list.php +++ b/apps/files_sharing/list.php @@ -33,6 +33,7 @@ $tmpl = new OCP\Template('files_sharing', 'list', ''); $tmpl->assign('showgridview', $showgridview && !$isIE); OCP\Util::addScript('files_sharing', 'dist/files_sharing'); +OCP\Util::addScript('files_sharing', 'dist/files_sharing_tab'); \OC::$server->getEventDispatcher()->dispatch('\OCP\Collaboration\Resources::loadAdditionalScripts'); $tmpl->printPage(); diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue new file mode 100644 index 0000000000..857b57adbd --- /dev/null +++ b/apps/files_sharing/src/components/SharingEntry.vue @@ -0,0 +1,249 @@ + + + + + + + diff --git a/apps/files_sharing/src/components/SharingEntryInternal.vue b/apps/files_sharing/src/components/SharingEntryInternal.vue new file mode 100644 index 0000000000..720c016b82 --- /dev/null +++ b/apps/files_sharing/src/components/SharingEntryInternal.vue @@ -0,0 +1,117 @@ + + + + + + diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue new file mode 100644 index 0000000000..afeaee06bd --- /dev/null +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -0,0 +1,769 @@ + + + + + + + diff --git a/apps/files_sharing/src/components/SharingEntrySimple.vue b/apps/files_sharing/src/components/SharingEntrySimple.vue new file mode 100644 index 0000000000..4538950a83 --- /dev/null +++ b/apps/files_sharing/src/components/SharingEntrySimple.vue @@ -0,0 +1,97 @@ + + + + + + + diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue new file mode 100644 index 0000000000..df222eafe0 --- /dev/null +++ b/apps/files_sharing/src/components/SharingInput.vue @@ -0,0 +1,444 @@ + + + + + + + diff --git a/apps/files_sharing/src/files_sharing_tab.js b/apps/files_sharing/src/files_sharing_tab.js new file mode 100644 index 0000000000..18b4f4d7d1 --- /dev/null +++ b/apps/files_sharing/src/files_sharing_tab.js @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import SharingTab from './views/SharingTab' +import ShareSearch from './services/ShareSearch' +import ExternalLinkActions from './services/ExternalLinkActions' + +if (window.OCA && window.OCA.Sharing) { + Object.assign(window.OCA.Sharing, { ShareSearch: new ShareSearch() }) +} + +if (window.OCA && window.OCA.Sharing) { + Object.assign(window.OCA.Sharing, { ExternalLinkActions: new ExternalLinkActions() }) +} + +window.addEventListener('DOMContentLoaded', () => { + if (OCA.Files && OCA.Files.Sidebar) { + OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('sharing', SharingTab)) + } +}) diff --git a/apps/files_sharing/src/mixins/ShareRequests.js b/apps/files_sharing/src/mixins/ShareRequests.js new file mode 100644 index 0000000000..c534e86070 --- /dev/null +++ b/apps/files_sharing/src/mixins/ShareRequests.js @@ -0,0 +1,114 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +// TODO: remove when ie not supported +import 'url-search-params-polyfill' + +import { generateOcsUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' +import Share from '../models/Share' + +const shareUrl = generateOcsUrl('apps/files_sharing/api/v1', 2) + 'shares' +const headers = { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' +} + +export default { + methods: { + /** + * Create a new share + * + * @param {Object} data destructuring object + * @param {string} data.path path to the file/folder which should be shared + * @param {number} data.shareType 0 = user; 1 = group; 3 = public link; 6 = federated cloud share + * @param {string} data.shareWith user/group id with which the file should be shared (optional for shareType > 1) + * @param {boolean} [data.publicUpload=false] allow public upload to a public shared folder + * @param {string} [data.password] password to protect public link Share with + * @param {number} [data.permissions=31] 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1) + * @param {boolean} [data.sendPasswordByTalk=false] send the password via a talk conversation + * @param {string} [data.expireDate=''] expire the shareautomatically after + * @param {string} [data.label=''] custom label + * @returns {Share} the new share + * @throws {Error} + */ + async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label }) { + try { + const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label }) + if (!('ocs' in request.data)) { + throw request + } + return new Share(request.data.ocs.data) + } catch (error) { + console.error('Error while creating share', error) + OC.Notification.showTemporary(t('files_sharing', 'Error creating the share'), { type: 'error' }) + throw error + } + }, + + /** + * Delete a share + * + * @param {number} id share id + * @throws {Error} + */ + async deleteShare(id) { + try { + const request = await axios.delete(shareUrl + `/${id}`) + if (!('ocs' in request.data)) { + throw request + } + return true + } catch (error) { + console.error('Error while deleting share', error) + OC.Notification.showTemporary(t('files_sharing', 'Error deleting the share'), { type: 'error' }) + throw error + } + }, + + /** + * Update a share + * + * @param {number} id share id + * @param {Object} data destructuring object + * @param {string} data.property property to update + * @param {any} data.value value to set + */ + async updateShare(id, { property, value }) { + try { + // ocs api requires x-www-form-urlencoded + const data = new URLSearchParams() + data.append(property, value) + + const request = await axios.put(shareUrl + `/${id}`, { [property]: value }, headers) + if (!('ocs' in request.data)) { + throw request + } + return true + } catch (error) { + console.error('Error while updating share', error) + OC.Notification.showTemporary(t('files_sharing', 'Error updating the share'), { type: 'error' }) + const message = error.response.data.ocs.meta.message + throw new Error(`${property}, ${message}`) + } + } + } +} diff --git a/apps/files_sharing/src/mixins/ShareTypes.js b/apps/files_sharing/src/mixins/ShareTypes.js new file mode 100644 index 0000000000..81e6af7d97 --- /dev/null +++ b/apps/files_sharing/src/mixins/ShareTypes.js @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default { + data() { + return { + SHARE_TYPES: { + SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER, + SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP, + SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK, + SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL, + SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE, + SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE, + SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST, + SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP, + SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM + } + } + } +} diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js new file mode 100644 index 0000000000..d012f35591 --- /dev/null +++ b/apps/files_sharing/src/mixins/SharesMixin.js @@ -0,0 +1,303 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +import PQueue from 'p-queue' +import debounce from 'debounce' + +import Share from '../models/Share' +import SharesRequests from './ShareRequests' +import ShareTypes from './ShareTypes' +import Config from '../services/ConfigService' +import { getCurrentUser } from '@nextcloud/auth' + +export default { + mixins: [SharesRequests, ShareTypes], + + props: { + fileInfo: { + type: Object, + default: () => {}, + required: true + }, + share: { + type: Share, + default: null + } + }, + + data() { + return { + config: new Config(), + + // errors helpers + errors: {}, + + // component status toggles + loading: false, + saving: false, + open: false, + + // concurrency management queue + // we want one queue per share + updateQueue: new PQueue({ concurrency: 1 }), + + /** + * ! This allow vue to make the Share class state reactive + * ! do not remove it ot you'll lose all reactivity here + */ + reactiveState: this.share && this.share.state, + + SHARE_TYPES: { + SHARE_TYPE_USER: OC.Share.SHARE_TYPE_USER, + SHARE_TYPE_GROUP: OC.Share.SHARE_TYPE_GROUP, + SHARE_TYPE_LINK: OC.Share.SHARE_TYPE_LINK, + SHARE_TYPE_EMAIL: OC.Share.SHARE_TYPE_EMAIL, + SHARE_TYPE_REMOTE: OC.Share.SHARE_TYPE_REMOTE, + SHARE_TYPE_CIRCLE: OC.Share.SHARE_TYPE_CIRCLE, + SHARE_TYPE_GUEST: OC.Share.SHARE_TYPE_GUEST, + SHARE_TYPE_REMOTE_GROUP: OC.Share.SHARE_TYPE_REMOTE_GROUP, + SHARE_TYPE_ROOM: OC.Share.SHARE_TYPE_ROOM + } + } + }, + + computed: { + + /** + * Does the current share have an expiration date + * @returns {boolean} + */ + hasExpirationDate: { + get: function() { + return this.config.isDefaultExpireDateEnforced || !!this.share.expireDate + }, + set: function(enabled) { + this.share.expireDate = enabled + ? this.config.defaultExpirationDateString !== '' + ? this.config.defaultExpirationDateString + : moment().format('YYYY-MM-DD') + : '' + } + }, + + /** + * Does the current share have a note + * @returns {boolean} + */ + hasNote: { + get: function() { + return !!this.share.note + }, + set: function(enabled) { + this.share.note = enabled + ? t('files_sharing', 'Enter a note for the share recipient') + : '' + } + }, + + dateTomorrow() { + return moment().add(1, 'days') + }, + + dateMaxEnforced() { + return this.config.isDefaultExpireDateEnforced + && moment().add(1 + this.config.defaultExpireDate, 'days') + }, + + /** + * Datepicker lang values + * https://github.com/nextcloud/nextcloud-vue/pull/146 + * TODO: have this in vue-components + * + * @returns {int} + */ + firstDay() { + return window.firstDay + ? window.firstDay + : 0 // sunday as default + }, + lang() { + // fallback to default in case of unavailable data + return { + days: window.dayNamesShort + ? window.dayNamesShort // provided by nextcloud + : ['Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.'], + months: window.monthNamesShort + ? window.monthNamesShort // provided by nextcloud + : ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'], + placeholder: { + date: 'Select Date' // TODO: Translate + } + } + }, + + isShareOwner() { + return this.share && this.share.owner === getCurrentUser().uid + } + + }, + + methods: { + /** + * Check if a share is valid before + * firing the request + * + * @param {Share} share the share to check + * @returns {Boolean} + */ + checkShare(share) { + if (share.password) { + if (typeof share.password !== 'string' || share.password.trim() === '') { + return false + } + } + if (share.expirationDate) { + const date = moment(share.expirationDate) + if (!date.isValid()) { + return false + } + } + return true + }, + + /** + * ActionInput can be a little tricky to work with. + * Since we expect a string and not a Date, + * we need to process the value here + * + * @param {Date} date js date to be parsed by moment.js + */ + onExpirationChange(date) { + // format to YYYY-MM-DD + const value = moment(date).format('YYYY-MM-DD') + this.share.expireDate = value + this.queueUpdate('expireDate') + }, + + /** + * Uncheck expire date + * We need this method because @update:checked + * is ran simultaneously as @uncheck, so + * so we cannot ensure data is up-to-date + */ + onExpirationDisable() { + this.share.expireDate = '' + this.queueUpdate('expireDate') + }, + + /** + * Delete share button handler + */ + async onDelete() { + try { + this.loading = true + this.open = false + await this.deleteShare(this.share.id) + console.debug('Share deleted', this.share.id) + this.$emit('remove:share', this.share) + } catch (error) { + // re-open menu if error + this.open = true + } finally { + this.loading = false + } + }, + + /** + * Send an update of the share to the queue + * + * @param {string} property the property to sync + */ + queueUpdate(property) { + if (this.share.id) { + // force value to string because that is what our + // share api controller accepts + const value = this.share[property].toString() + + this.updateQueue.add(async() => { + this.saving = true + this.errors = {} + try { + await this.updateShare(this.share.id, { + property, + value + }) + + // clear any previous errors + this.$delete(this.errors, property) + + // reset password state after sync + this.$delete(this.share, 'newPassword') + } catch ({ property, message }) { + this.onSyncError(property, message) + } finally { + this.saving = false + } + }) + } else { + console.error('Cannot update share.', this.share, 'No valid id') + } + }, + + /** + * Manage sync errors + * @param {string} property the errored property, e.g. 'password' + * @param {string} message the error message + */ + onSyncError(property, message) { + // re-open menu if closed + this.open = true + switch (property) { + case 'password': + case 'pending': + case 'expireDate': + case 'note': { + // show error + this.$set(this.errors, property, message) + + let propertyEl = this.$refs[property] + if (propertyEl) { + if (propertyEl.$el) { + propertyEl = propertyEl.$el + } + // focus if there is a focusable action element + const focusable = propertyEl.querySelector('.focusable') + if (focusable) { + focusable.focus() + } + } + break + } + } + }, + + /** + * Debounce queueUpdate to avoid requests spamming + * more importantly for text data + * + * @param {string} property the property to sync + */ + debounceQueueUpdate: debounce(function(property) { + this.queueUpdate(property) + }, 500) + } +} diff --git a/apps/files_sharing/src/models/Share.js b/apps/files_sharing/src/models/Share.js new file mode 100644 index 0000000000..e9d84fb555 --- /dev/null +++ b/apps/files_sharing/src/models/Share.js @@ -0,0 +1,444 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default class Share { + + #share; + + /** + * Create the share object + * + * @param {Object} ocsData ocs request response + */ + constructor(ocsData) { + if (ocsData.ocs && ocsData.ocs.data && ocsData.ocs.data[0]) { + ocsData = ocsData.ocs.data[0] + } + + // convert int into boolean + ocsData.hide_download = !!ocsData.hide_download + ocsData.mail_send = !!ocsData.mail_send + + // store state + this.#share = ocsData + } + + /** + * Get the share state + * ! used for reactivity purpose + * Do not remove. It allow vuejs to + * inject its watchers into the #share + * state and make the whole class reactive + * + * @returns {Object} the share raw state + * @readonly + * @memberof Sidebar + */ + get state() { + return this.#share + } + + /** + * get the share id + * + * @returns {int} + * @readonly + * @memberof Share + */ + get id() { + return this.#share.id + } + + /** + * Get the share type + * + * @returns {int} + * @readonly + * @memberof Share + */ + get type() { + return this.#share.share_type + } + + /** + * Get the share permissions + * See OC.PERMISSION_* variables + * + * @returns {int} + * @readonly + * @memberof Share + */ + get permissions() { + return this.#share.permissions + } + + /** + * Set the share permissions + * See OC.PERMISSION_* variables + * + * @param {int} permissions valid permission, See OC.PERMISSION_* variables + * @memberof Share + */ + set permissions(permissions) { + this.#share.permissions = permissions + } + + // SHARE OWNER -------------------------------------------------- + /** + * Get the share owner uid + * + * @returns {string} + * @readonly + * @memberof Share + */ + get owner() { + return this.#share.uid_owner + } + + /** + * Get the share owner's display name + * + * @returns {string} + * @readonly + * @memberof Share + */ + get ownerDisplayName() { + return this.#share.displayname_owner + } + + // SHARED WITH -------------------------------------------------- + /** + * Get the share with entity uid + * + * @returns {string} + * @readonly + * @memberof Share + */ + get shareWith() { + return this.#share.share_with + } + + /** + * Get the share with entity display name + * fallback to its uid if none + * + * @returns {string} + * @readonly + * @memberof Share + */ + get shareWithDisplayName() { + return this.#share.share_with_displayname + || this.#share.share_with + } + + /** + * Get the share with avatar if any + * + * @returns {string} + * @readonly + * @memberof Share + */ + get shareWithAvatar() { + return this.#share.share_with_avatar + } + + // SHARED FILE OR FOLDER OWNER ---------------------------------- + /** + * Get the shared item owner uid + * + * @returns {string} + * @readonly + * @memberof Share + */ + get uidFileOwner() { + return this.#share.uid_file_owner + } + + /** + * Get the shared item display name + * fallback to its uid if none + * + * @returns {string} + * @readonly + * @memberof Share + */ + get displaynameFileOwner() { + return this.#share.displayname_file_owner + || this.#share.uid_file_owner + } + + // TIME DATA ---------------------------------------------------- + /** + * Get the share creation timestamp + * + * @returns {int} + * @readonly + * @memberof Share + */ + get createdTime() { + return this.#share.stime + } + + /** + * Get the expiration date as a string format + * + * @returns {string} + * @readonly + * @memberof Share + */ + get expireDate() { + return this.#share.expiration + } + + /** + * Set the expiration date as a string format + * e.g. YYYY-MM-DD + * + * @param {string} date the share expiration date + * @memberof Share + */ + set expireDate(date) { + this.#share.expiration = date + } + + // EXTRA DATA --------------------------------------------------- + /** + * Get the public share token + * + * @returns {string} the token + * @readonly + * @memberof Share + */ + get token() { + return this.#share.token + } + + /** + * Get the share note if any + * + * @returns {string} + * @readonly + * @memberof Share + */ + get note() { + return this.#share.note + } + + /** + * Set the share note if any + * + * @param {string} note the note + * @memberof Share + */ + set note(note) { + this.#share.note = note.trim() + } + + /** + * Have a mail been sent + * + * @returns {boolean} + * @readonly + * @memberof Share + */ + get mailSend() { + return this.#share.mail_send === true + } + + /** + * Hide the download button on public page + * + * @returns {boolean} + * @readonly + * @memberof Share + */ + get hideDownload() { + return this.#share.hide_download === true + } + + /** + * Hide the download button on public page + * + * @param {boolean} state hide the button ? + * @memberof Share + */ + set hideDownload(state) { + this.#share.hide_download = state === true + } + + /** + * Password protection of the share + * + * @returns {string} + * @readonly + * @memberof Share + */ + get password() { + return this.#share.password + } + + /** + * Password protection of the share + * + * @param {string} password the share password + * @memberof Share + */ + set password(password) { + this.#share.password = password.trim() + } + + // SHARED ITEM DATA --------------------------------------------- + /** + * Get the shared item absolute full path + * + * @returns {string} + * @readonly + * @memberof Share + */ + get path() { + return this.#share.path + } + + /** + * Return the item type: file or folder + * + * @returns {string} 'folder' or 'file' + * @readonly + * @memberof Share + */ + get itemType() { + return this.#share.item_type + } + + /** + * Get the shared item mimetype + * + * @returns {string} + * @readonly + * @memberof Share + */ + get mimetype() { + return this.#share.mimetype + } + + /** + * Get the shared item id + * + * @returns {int} + * @readonly + * @memberof Share + */ + get fileSource() { + return this.#share.file_source + } + + /** + * Get the target path on the receiving end + * e.g the file /xxx/aaa will be shared in + * the receiving root as /aaa, the fileTarget is /aaa + * + * @returns {string} + * @readonly + * @memberof Share + */ + get fileTarget() { + return this.#share.file_target + } + + /** + * Get the parent folder id if any + * + * @returns {int} + * @readonly + * @memberof Share + */ + get fileParent() { + return this.#share.file_parent + } + + // PERMISSIONS Shortcuts + /** + * Does this share have CREATE permissions + * + * @returns {boolean} + * @readonly + * @memberof Share + */ + get hasCreatePermission() { + return !!((this.permissions & OC.PERMISSION_CREATE)) + } + + /** + * Does this share have DELETE permissions + * + * @returns {boolean} + * @readonly + * @memberof Share + */ + get hasDeletePermission() { + return !!((this.permissions & OC.PERMISSION_DELETE)) + } + + /** + * Does this share have UPDATE permissions + * + * @returns {boolean} + * @readonly + * @memberof Share + */ + get hasUpdatePermission() { + return !!((this.permissions & OC.PERMISSION_UPDATE)) + } + + /** + * Does this share have SHARE permissions + * + * @returns {boolean} + * @readonly + * @memberof Share + */ + get hasSharePermission() { + return !!((this.permissions & OC.PERMISSION_SHARE)) + } + + // TODO: SORT THOSE PROPERTIES + get label() { + return this.#share.label + } + + get parent() { + return this.#share.parent + } + + get storageId() { + return this.#share.storage_id + } + + get storage() { + return this.#share.storage + } + + get itemSource() { + return this.#share.item_source + } + +} diff --git a/apps/files_sharing/src/services/ConfigService.js b/apps/files_sharing/src/services/ConfigService.js new file mode 100644 index 0000000000..7058c71477 --- /dev/null +++ b/apps/files_sharing/src/services/ConfigService.js @@ -0,0 +1,223 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default class Config { + + /** + * Is public upload allowed on link shares ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isPublicUploadEnabled() { + return document.getElementById('filestable') + && document.getElementById('filestable').dataset.allowPublicUpload === 'yes' + } + + /** + * Are link share allowed ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isShareWithLinkAllowed() { + return document.getElementById('allowShareWithLink') + && document.getElementById('allowShareWithLink').value === 'yes' + } + + /** + * Get the federated sharing documentation link + * + * @returns {string} + * @readonly + * @memberof Config + */ + get federatedShareDocLink() { + return OC.appConfig.core.federatedCloudShareDoc + } + + /** + * Get the default expiration date as string + * + * @returns {string} + * @readonly + * @memberof Config + */ + get defaultExpirationDateString() { + let expireDateString = '' + if (this.isDefaultExpireDateEnabled) { + const date = window.moment.utc() + const expireAfterDays = this.defaultExpireDate + date.add(expireAfterDays, 'days') + expireDateString = date.format('YYYY-MM-DD') + } + return expireDateString + } + + /** + * Are link shares password-enforced ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get enforcePasswordForPublicLink() { + return OC.appConfig.core.enforcePasswordForPublicLink === true + } + + /** + * Is password asked by default on link shares ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get enableLinkPasswordByDefault() { + return OC.appConfig.core.enableLinkPasswordByDefault === true + } + + /** + * Is link shares expiration enforced ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isDefaultExpireDateEnforced() { + return OC.appConfig.core.defaultExpireDateEnforced === true + } + + /** + * Is there a default expiration date for new link shares ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isDefaultExpireDateEnabled() { + return OC.appConfig.core.defaultExpireDateEnabled === true + } + + /** + * Are users on this server allowed to send shares to other servers ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isRemoteShareAllowed() { + return OC.appConfig.core.remoteShareAllowed === true + } + + /** + * Is sharing my mail (link share) enabled ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isMailShareAllowed() { + return OC.appConfig.shareByMailEnabled !== undefined + } + + /** + * Get the default days to expiration + * + * @returns {int} + * @readonly + * @memberof Config + */ + get defaultExpireDate() { + return OC.appConfig.core.defaultExpireDate + } + + /** + * Is resharing allowed ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isResharingAllowed() { + return OC.appConfig.core.resharingAllowed === true + } + + /** + * Is password enforced for mail shares ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get isPasswordForMailSharesRequired() { + return (OC.appConfig.shareByMail === undefined) ? false : OC.appConfig.shareByMail.enforcePasswordProtection === true + } + + /** + * Is sharing with groups allowed ? + * + * @returns {boolean} + * @readonly + * @memberof Config + */ + get allowGroupSharing() { + return OC.appConfig.core.allowGroupSharing === true + } + + /** + * Get the maximum results of a share search + * + * @returns {int} + * @readonly + * @memberof Config + */ + get maxAutocompleteResults() { + return parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 200 + } + + /** + * Get the minimal string length + * to initiate a share search + * + * @returns {int} + * @readonly + * @memberof Config + */ + get minSearchStringLength() { + return parseInt(OC.config['sharing.minSearchStringLength'], 10) || 0 + } + + /** + * Get the password policy config + * + * @returns {Object} + * @readonly + * @memberof Config + */ + get passwordPolicy() { + const capabilities = OC.getCapabilities() + return capabilities.password_policy ? capabilities.password_policy : {} + } + +} diff --git a/apps/files_sharing/src/services/ExternalLinkActions.js b/apps/files_sharing/src/services/ExternalLinkActions.js new file mode 100644 index 0000000000..f67a1cb115 --- /dev/null +++ b/apps/files_sharing/src/services/ExternalLinkActions.js @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default class ExternalLinkActions { + + #state; + + constructor() { + // init empty state + this.#state = {} + + // init default values + this.#state.actions = [] + console.debug('OCA.Sharing.ExternalLinkActions initialized') + } + + /** + * Get the state + * + * @readonly + * @memberof ExternalLinkActions + * @returns {Object} the data state + */ + get state() { + return this.#state + } + + /** + * Register a new action for the link share + * Mostly used by the social sharing app. + * + * @param {Object} action new action component to register + * @returns {boolean} + */ + registerAction(action) { + if (typeof action === 'object' && action.render && action.components) { + this.#state.actions.push(action) + return true + } + console.error(`Invalid action component provided`, action) + return false + } + +} diff --git a/apps/files_sharing/src/services/ShareSearch.js b/apps/files_sharing/src/services/ShareSearch.js new file mode 100644 index 0000000000..dda1feb30a --- /dev/null +++ b/apps/files_sharing/src/services/ShareSearch.js @@ -0,0 +1,71 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +export default class ShareSearch { + + #state; + + constructor() { + // init empty state + this.#state = {} + + // init default values + this.#state.results = [] + console.debug('OCA.Sharing.ShareSearch initialized') + } + + /** + * Get the state + * + * @readonly + * @memberof ShareSearch + * @returns {Object} the data state + */ + get state() { + return this.#state + } + + /** + * Register a new result + * Mostly used by the guests app. + * We should consider deprecation and add results via php ? + * + * @param {Object} result entry to append + * @param {string} [result.user] entry user + * @param {string} result.displayName entry first line + * @param {string} [result.desc] entry second line + * @param {string} [result.icon] entry icon + * @param {function} result.handler function to run on entry selection + * @param {function} [result.condition] condition to add entry or not + * @returns {boolean} + */ + addNewResult(result) { + if (result.displayName.trim() !== '' + && typeof result.handler === 'function') { + this.#state.results.push(result) + return true + } + console.error(`Invalid search result provided`, result) + return false + } + +} diff --git a/apps/files_sharing/src/share.js b/apps/files_sharing/src/share.js index a66f166759..46e46e3755 100644 --- a/apps/files_sharing/src/share.js +++ b/apps/files_sharing/src/share.js @@ -195,7 +195,7 @@ // do not open sidebar if permission is set and equal to 0 var permissions = parseInt(context.$file.data('share-permissions'), 10) if (isNaN(permissions) || permissions > 0) { - fileList.showDetailsView(fileName, 'shareTabView') + fileList.showDetailsView(fileName, 'sharing') } }, render: function(actionSpec, isDefault, context) { @@ -209,37 +209,37 @@ } }) - var shareTab = new OCA.Sharing.ShareTabView('shareTabView', { order: -20 }) - // detect changes and change the matching list entry - shareTab.on('sharesChanged', function(shareModel) { - var fileInfoModel = shareModel.fileInfoModel - var $tr = fileList.findFileEl(fileInfoModel.get('name')) + var shareTab = new OCA.Sharing.ShareTabView('sharing', {order: -20}) + // // detect changes and change the matching list entry + // shareTab.on('sharesChanged', function(shareModel) { + // var fileInfoModel = shareModel.fileInfoModel + // var $tr = fileList.findFileEl(fileInfoModel.get('name')) - // We count email shares as link share - var hasLinkShares = shareModel.hasLinkShares() - shareModel.get('shares').forEach(function(share) { - if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { - hasLinkShares = true - } - }) + // // We count email shares as link share + // var hasLinkShares = shareModel.hasLinkShares(); + // shareModel.get('shares').forEach(function (share) { + // if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { + // hasLinkShares = true; + // } + // }) - OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel) - if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) { - // remove icon, if applicable - OC.Share.markFileAsShared($tr, false, false) - } + // OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel); + // if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) { + // // remove icon, if applicable + // OC.Share.markFileAsShared($tr, false, false) + // } - // FIXME: this is too convoluted. We need to get rid of the above updates - // and only ever update the model and let the events take care of rerendering - fileInfoModel.set({ - shareTypes: shareModel.getShareTypes(), - // in case markFileAsShared decided to change the icon, - // we need to modify the model - // (FIXME: yes, this is hacky) - icon: $tr.attr('data-icon') - }) - }) - fileList.registerTabView(shareTab) + // // FIXME: this is too convoluted. We need to get rid of the above updates + // // and only ever update the model and let the events take care of rerendering + // fileInfoModel.set({ + // shareTypes: shareModel.getShareTypes(), + // // in case markFileAsShared decided to change the icon, + // // we need to modify the model + // // (FIXME: yes, this is hacky) + // icon: $tr.attr('data-icon') + // }) + // }) + // fileList.registerTabView(shareTab) var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab }) fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView) diff --git a/apps/files_sharing/src/sharebreadcrumbview.js b/apps/files_sharing/src/sharebreadcrumbview.js index a90c94b6d7..c712229b2e 100644 --- a/apps/files_sharing/src/sharebreadcrumbview.js +++ b/apps/files_sharing/src/sharebreadcrumbview.js @@ -93,7 +93,7 @@ dirInfo: self._dirInfo }) }) - OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView') + OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'sharing') } }) diff --git a/apps/files_sharing/src/utils/SharedWithMe.js b/apps/files_sharing/src/utils/SharedWithMe.js new file mode 100644 index 0000000000..b2e2e34a9b --- /dev/null +++ b/apps/files_sharing/src/utils/SharedWithMe.js @@ -0,0 +1,86 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ + +/** + * Get the shared with me title + * + * @param {Share} share current share + * @returns {string} the title + */ +const shareWithTitle = function(share) { + if (share.type === OC.Share.type_GROUP) { + return t( + 'files_sharing', + 'Shared with you and the group {group} by {owner}', + { + group: share.shareWithDisplayName, + owner: share.ownerDisplayName + }, + undefined, + { escape: false } + ) + } else if (share.type === OC.Share.type_CIRCLE) { + return t( + 'files_sharing', + 'Shared with you and {circle} by {owner}', + { + circle: share.shareWithDisplayName, + owner: share.ownerDisplayName + }, + undefined, + { escape: false } + ) + } else if (share.type === OC.Share.type_ROOM) { + if (this.model.get('reshare').share_with_displayname) { + return t( + 'files_sharing', + 'Shared with you and the conversation {conversation} by {owner}', + { + conversation: share.shareWithDisplayName, + owner: share.ownerDisplayName + }, + undefined, + { escape: false } + ) + } else { + return t( + 'files_sharing', + 'Shared with you in a conversation by {owner}', + { + owner: share.ownerDisplayName + }, + undefined, + { escape: false } + ) + } + } else { + return t( + 'files_sharing', + 'Shared with you by {owner}', + { owner: share.ownerDisplayName }, + undefined, + { escape: false } + ) + } +} + +export { shareWithTitle } diff --git a/apps/files_sharing/src/views/SharingLinkList.vue b/apps/files_sharing/src/views/SharingLinkList.vue new file mode 100644 index 0000000000..1c01886ca4 --- /dev/null +++ b/apps/files_sharing/src/views/SharingLinkList.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/apps/files_sharing/src/views/SharingList.vue b/apps/files_sharing/src/views/SharingList.vue new file mode 100644 index 0000000000..c2ecbbbd1a --- /dev/null +++ b/apps/files_sharing/src/views/SharingList.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue new file mode 100644 index 0000000000..5a9b24c36b --- /dev/null +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -0,0 +1,318 @@ + + + + + + + diff --git a/apps/files_sharing/webpack.js b/apps/files_sharing/webpack.js index 3fc0628b20..43a34559d4 100644 --- a/apps/files_sharing/webpack.js +++ b/apps/files_sharing/webpack.js @@ -4,6 +4,7 @@ module.exports = { entry: { 'additionalScripts': path.join(__dirname, 'src', 'additionalScripts.js'), 'files_sharing': path.join(__dirname, 'src', 'files_sharing.js'), + 'files_sharing_tab': path.join(__dirname, 'src', 'files_sharing_tab.js'), 'collaboration': path.join(__dirname, 'src', 'collaborationresourceshandler.js'), }, output: { diff --git a/core/js/files/client.js b/core/js/files/client.js index 98874d165b..0daf7c9dc3 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -323,6 +323,13 @@ data.isEncrypted = false; } + var isFavouritedProp = props['{' + Client.NS_OWNCLOUD + '}favorite']; + if (!_.isUndefined(isFavouritedProp)) { + data.isFavourited = isFavouritedProp === '1'; + } else { + data.isFavourited = false; + } + var contentType = props[Client.PROPERTY_GETCONTENTTYPE]; if (!_.isUndefined(contentType)) { data.mimetype = contentType; diff --git a/core/src/Polyfill/closest.js b/core/src/Polyfill/closest.js new file mode 100644 index 0000000000..1c60864612 --- /dev/null +++ b/core/src/Polyfill/closest.js @@ -0,0 +1,19 @@ +// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + +if (!Element.prototype.matches) { + Element.prototype.matches + = Element.prototype.msMatchesSelector + || Element.prototype.webkitMatchesSelector +} + +if (!Element.prototype.closest) { + Element.prototype.closest = function(s) { + var el = this + + do { + if (el.matches(s)) return el + el = el.parentElement || el.parentNode + } while (el !== null && el.nodeType === 1) + return null + } +} diff --git a/core/src/Polyfill/index.js b/core/src/Polyfill/index.js index 055ab6343a..306c72a077 100644 --- a/core/src/Polyfill/index.js +++ b/core/src/Polyfill/index.js @@ -1,4 +1,4 @@ -/* +/** * @copyright 2019 Christoph Wurst * * @author 2019 Christoph Wurst @@ -20,4 +20,5 @@ */ import './console' +import './closest' import './windows-phone' diff --git a/core/src/main.js b/core/src/main.js index 29c657f5db..3f0e82df95 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -1,4 +1,4 @@ -/* +/** * @copyright 2018 Christoph Wurst * * @author 2018 Christoph Wurst @@ -20,8 +20,8 @@ */ import $ from 'jquery' -import '@babel/polyfill' import './Polyfill/index' +import '@babel/polyfill' // If you remove the line below, tests won't pass // eslint-disable-next-line no-unused-vars diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue index 08538ec2fe..c7958aac15 100644 --- a/core/src/views/Login.vue +++ b/core/src/views/Login.vue @@ -27,7 +27,7 @@ =10.0.0" + } } diff --git a/webpack.common.js b/webpack.common.js index 3264514606..53c5d5e676 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -3,10 +3,10 @@ const path = require('path') const merge = require('webpack-merge') const { VueLoaderPlugin } = require('vue-loader') -const core = require('./core/webpack') - const accessibility = require('./apps/accessibility/webpack') const comments = require('./apps/comments/webpack') +const core = require('./core/webpack') +const files = require('./apps/files/webpack') const files_sharing = require('./apps/files_sharing/webpack') const files_trashbin = require('./apps/files_trashbin/webpack') const files_versions = require('./apps/files_versions/webpack') @@ -18,14 +18,15 @@ const updatenotifications = require('./apps/updatenotification/webpack') const workflowengine = require('./apps/workflowengine/webpack') const modules = { - core, - settings, accessibility, comments, + core, + files, files_sharing, files_trashbin, files_versions, oauth2, + settings, systemtags, twofactor_backupscodes, updatenotifications, From 515171a653d92c292070ef047c91fd724e0de45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Thu, 23 May 2019 17:03:04 +0200 Subject: [PATCH 03/14] Transpile also dependencies in node_modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some of the dependencies in node_modules, such as "p-queue", are not ES5 compatible, so they need to be transpiled to work in older browsers like Internet Explorer 11. Besides not excluding the dependencies for babel-loader in "webpack.common.js" the global Babel configuration must be defined in "babel.config.js", as in Babel 7.X, when ".babelrc.js" is used, all the dependencies in "node_modules" are ignored (even if whitelisted in the configuration file itself). Signed-off-by: John Molakvoæ (skjnldsv) --- .babelrc.js => babel.config.js | 0 build/files-checker.php | 2 +- webpack.common.js | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) rename .babelrc.js => babel.config.js (100%) diff --git a/.babelrc.js b/babel.config.js similarity index 100% rename from .babelrc.js rename to babel.config.js diff --git a/build/files-checker.php b/build/files-checker.php index a6a71e149e..ed9ff9ac5e 100644 --- a/build/files-checker.php +++ b/build/files-checker.php @@ -22,7 +22,6 @@ $expectedFiles = [ '.', '..', - '.babelrc.js', '.codecov.yml', '.drone.yml', '.eslintrc.js', @@ -46,6 +45,7 @@ $expectedFiles = [ 'autotest-external.sh', 'autotest-js.sh', 'autotest.sh', + 'babel.config.js', 'build', 'CHANGELOG.md', 'CODE_OF_CONDUCT.md', diff --git a/webpack.common.js b/webpack.common.js index 53c5d5e676..4a8cf1d2a0 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -81,7 +81,10 @@ module.exports = [] { test: /\.js$/, loader: 'babel-loader', - exclude: /node_modules/ + // automatically detect necessary packages to + // transpile in the node_modules folder + exclude: /node_modules(?!(\/|\\)(p-finally|p-limit|p-locate|p-queue|p-timeout|p-try)(\/|\\))/ + }, { test: /\.(png|jpg|gif)$/, From adb163b33798313f20b7bb34c0fc2a9f8268c87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 9 Jul 2019 12:49:09 +0200 Subject: [PATCH 04/14] Add projects view to sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files_sharing/src/views/SharingTab.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index 5a9b24c36b..4f16f4b243 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -63,6 +63,12 @@ + + + @@ -72,6 +78,7 @@ import { generateOcsUrl } from '@nextcloud/router' import Tab from 'nextcloud-vue/dist/Components/AppSidebarTab' import Avatar from 'nextcloud-vue/dist/Components/Avatar' import axios from '@nextcloud/axios' +import { CollectionList } from 'nextcloud-vue-collections' import { shareWithTitle } from '../utils/SharedWithMe' import Share from '../models/Share' @@ -88,6 +95,7 @@ export default { components: { Avatar, + CollectionList, SharingEntryInternal, SharingEntrySimple, SharingInput, From ae59edc6bfa7331636c7a0d0b144f292367ae5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 9 Jul 2019 15:00:41 +0200 Subject: [PATCH 05/14] Add ShareTabSections service to register sections in the share tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files_sharing/src/files_sharing_tab.js | 4 ++ .../files_sharing/src/services/TabSections.js | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 apps/files_sharing/src/services/TabSections.js diff --git a/apps/files_sharing/src/files_sharing_tab.js b/apps/files_sharing/src/files_sharing_tab.js index 18b4f4d7d1..076e4c8f3f 100644 --- a/apps/files_sharing/src/files_sharing_tab.js +++ b/apps/files_sharing/src/files_sharing_tab.js @@ -24,6 +24,8 @@ import SharingTab from './views/SharingTab' import ShareSearch from './services/ShareSearch' import ExternalLinkActions from './services/ExternalLinkActions' +import TabSections from './services/TabSections' + if (window.OCA && window.OCA.Sharing) { Object.assign(window.OCA.Sharing, { ShareSearch: new ShareSearch() }) } @@ -32,6 +34,8 @@ if (window.OCA && window.OCA.Sharing) { Object.assign(window.OCA.Sharing, { ExternalLinkActions: new ExternalLinkActions() }) } +Object.assign(window.OCA.Sharing, { ShareTabSections: new TabSections() }) + window.addEventListener('DOMContentLoaded', () => { if (OCA.Files && OCA.Files.Sidebar) { OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('sharing', SharingTab)) diff --git a/apps/files_sharing/src/services/TabSections.js b/apps/files_sharing/src/services/TabSections.js new file mode 100644 index 0000000000..0750cc707e --- /dev/null +++ b/apps/files_sharing/src/services/TabSections.js @@ -0,0 +1,49 @@ +/** + * @copyright Copyright (c) 2019 Julius Härtl + * + * @author Julius Härtl + * + * @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 . + * + */ + +/** + * Callback for adding two numbers. + * + * @callback registerSectionCallback + * @param {Element} el The DOM element where the section is rendered + * @param {FileInfo} fileInfo current file FileInfo + */ +export default class TabSections { + + #sections; + + constructor() { + this.#sections = [] + } + + /** + * @param {registerSectionCallback} section To be called to mount the section to the sharing sidebar + */ + registerSection(section) { + this.#sections.push(section) + } + + getSections() { + return this.#sections + } + +} From 1c13c52acffeac59b0ef5d156a9a5bc36e611619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Wed, 9 Oct 2019 07:53:30 +0200 Subject: [PATCH 06/14] Systemtags and external actions update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- apps/files/src/views/Sidebar.vue | 2 +- apps/files_sharing/src/components/SharingEntryLink.vue | 9 ++++++++- apps/files_sharing/src/services/ExternalLinkActions.js | 4 ++-- apps/files_sharing/src/views/SharingTab.vue | 8 ++++++++ apps/systemtags/src/systemtagsinfoview.js | 8 ++------ core/css/systemtags.scss | 2 ++ 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index 9a00df1737..e99389de75 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -113,7 +113,7 @@ export default { */ davPath() { const user = OC.getCurrentUser().uid - return OC.linkToRemote(`dav/files/${user}${encodeURIComponent(this.file)}`) + return OC.linkToRemote(`dav/files/${user}${this.file}`) }, /** diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index afeaee06bd..4501d67cbb 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -247,7 +247,14 @@ @update:value="debounceQueueUpdate('note')" /> - + + + {{ name }} + {{ t('files_sharing', 'Delete share') }} diff --git a/apps/files_sharing/src/services/ExternalLinkActions.js b/apps/files_sharing/src/services/ExternalLinkActions.js index f67a1cb115..35377d8676 100644 --- a/apps/files_sharing/src/services/ExternalLinkActions.js +++ b/apps/files_sharing/src/services/ExternalLinkActions.js @@ -52,11 +52,11 @@ export default class ExternalLinkActions { * @returns {boolean} */ registerAction(action) { - if (typeof action === 'object' && action.render && action.components) { + if (typeof action === 'object' && action.icon && action.name && action.url) { this.#state.actions.push(action) return true } - console.error(`Invalid action component provided`, action) + console.error(`Invalid action provided`, action) return false } diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue index 4f16f4b243..216b2e74ff 100644 --- a/apps/files_sharing/src/views/SharingTab.vue +++ b/apps/files_sharing/src/views/SharingTab.vue @@ -69,6 +69,14 @@ :id="`${fileInfo.id}`" type="file" :name="fileInfo.name" /> + + +
+ +
diff --git a/apps/systemtags/src/systemtagsinfoview.js b/apps/systemtags/src/systemtagsinfoview.js index 548a147591..2ec1ba0fef 100644 --- a/apps/systemtags/src/systemtagsinfoview.js +++ b/apps/systemtags/src/systemtagsinfoview.js @@ -30,7 +30,7 @@ _rendered: false, - className: 'systemTagsInfoView hidden', + className: 'systemTagsInfoView', /** * @type OC.SystemTags.SystemTagsInputField @@ -123,11 +123,7 @@ var appliedTags = collection.map(modelToSelection) self._inputView.setData(appliedTags) - if (appliedTags.length !== 0) { - self.show() - } else { - self.hide() - } + self.show() } }) } diff --git a/core/css/systemtags.scss b/core/css/systemtags.scss index bffafe101e..e2b587fcc7 100644 --- a/core/css/systemtags.scss +++ b/core/css/systemtags.scss @@ -69,8 +69,10 @@ } } +.systemTagsInfoView, .systemtags-select2-container { width: 100%; + .select2-choices .select2-search-choice.select2-locked .label { opacity: 0.5; } From d88b93c919b6bf7db1853ec91a68996e07a75ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Thu, 10 Oct 2019 11:26:15 +0200 Subject: [PATCH 07/14] Add LoadSidebar event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files/js/filelist.js | 9 ++++-- apps/files/lib/Controller/ViewController.php | 4 +++ apps/files/lib/Event/LoadSidebar.php | 31 +++++++++++++++++++ apps/files_sharing/src/share.js | 31 +------------------ 6 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 apps/files/lib/Event/LoadSidebar.php diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 3aa59c88b7..e241d34225 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -33,6 +33,7 @@ return array( 'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', 'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php', 'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => $baseDir . '/../lib/Event/LoadAdditionalScriptsEvent.php', + 'OCA\\Files\\Event\\LoadSidebar' => $baseDir . '/../lib/Event/LoadSidebar.php', 'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php', 'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php', 'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 07df2f173f..c4944e7b98 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -48,6 +48,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', 'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php', 'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => __DIR__ . '/..' . '/../lib/Event/LoadAdditionalScriptsEvent.php', + 'OCA\\Files\\Event\\LoadSidebar' => __DIR__ . '/..' . '/../lib/Event/LoadSidebar.php', 'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php', 'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php', 'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php', diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 8cca43d574..0424ba2006 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -636,9 +636,14 @@ fileName = '' } + // this is the old (terrible) way of getting the context. + // don't use it anywhere else. Just provide the full path + // of the file to the sidebar service + var tr = this.findFileEl(fileName) + var model = this.getModelForFile(tr) + var path = model.attributes.path + '/' + model.attributes.name + // open sidebar and set file - const dir = `${this.dirInfo.path}/${this.dirInfo.name}` - const path = `${dir}/${fileName}` OCA.Files.Sidebar.file = path.replace('//', '/') }, diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 518bbc1bf0..17caf06b48 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -9,6 +9,7 @@ * @author Thomas Müller * @author Vincent Petry * @author Felix Nüsse + * @author John Molakvoæ * * @license AGPL-3.0 * @@ -30,6 +31,7 @@ namespace OCA\Files\Controller; use OCA\Files\Activity\Helper; use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCA\Files\Event\LoadSidebar; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\RedirectResponse; @@ -269,6 +271,8 @@ class ViewController extends Controller { $event = new LoadAdditionalScriptsEvent(); $this->eventDispatcher->dispatch(LoadAdditionalScriptsEvent::class, $event); + $this->eventDispatcher->dispatch(LoadSidebar::class, new LoadSidebar()); + $params = []; $params['usedSpacePercent'] = (int) $storageInfo['relative']; $params['owner'] = $storageInfo['owner']; diff --git a/apps/files/lib/Event/LoadSidebar.php b/apps/files/lib/Event/LoadSidebar.php new file mode 100644 index 0000000000..8892bbe0f5 --- /dev/null +++ b/apps/files/lib/Event/LoadSidebar.php @@ -0,0 +1,31 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files\Event; + +use OCP\EventDispatcher\Event; + +class LoadSidebar extends Event { + +} diff --git a/apps/files_sharing/src/share.js b/apps/files_sharing/src/share.js index 46e46e3755..c7ef39897d 100644 --- a/apps/files_sharing/src/share.js +++ b/apps/files_sharing/src/share.js @@ -209,37 +209,8 @@ } }) + // register share breadcrumbs component var shareTab = new OCA.Sharing.ShareTabView('sharing', {order: -20}) - // // detect changes and change the matching list entry - // shareTab.on('sharesChanged', function(shareModel) { - // var fileInfoModel = shareModel.fileInfoModel - // var $tr = fileList.findFileEl(fileInfoModel.get('name')) - - // // We count email shares as link share - // var hasLinkShares = shareModel.hasLinkShares(); - // shareModel.get('shares').forEach(function (share) { - // if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { - // hasLinkShares = true; - // } - // }) - - // OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel); - // if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) { - // // remove icon, if applicable - // OC.Share.markFileAsShared($tr, false, false) - // } - - // // FIXME: this is too convoluted. We need to get rid of the above updates - // // and only ever update the model and let the events take care of rerendering - // fileInfoModel.set({ - // shareTypes: shareModel.getShareTypes(), - // // in case markFileAsShared decided to change the icon, - // // we need to modify the model - // // (FIXME: yes, this is hacky) - // icon: $tr.attr('data-icon') - // }) - // }) - // fileList.registerTabView(shareTab) var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab }) fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView) From 480691a56951e85bf32c86e43e2d3e93dad03560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Mon, 14 Oct 2019 22:55:58 +0200 Subject: [PATCH 08/14] Prevent multiple systemtags views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- apps/files/src/services/Sidebar.js | 2 +- apps/systemtags/src/systemtagsinfoview.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/files/src/services/Sidebar.js b/apps/files/src/services/Sidebar.js index 8f02a1b51a..2bb836272f 100644 --- a/apps/files/src/services/Sidebar.js +++ b/apps/files/src/services/Sidebar.js @@ -67,7 +67,7 @@ export default class Sidebar { } registerSecondaryView(view) { - const hasDuplicate = this.#state.views.findIndex(check => check.cid === view.cid) > -1 + const hasDuplicate = this.#state.views.findIndex(check => check.name === view.name) > -1 if (!hasDuplicate) { this.#state.views.push(view) return true diff --git a/apps/systemtags/src/systemtagsinfoview.js b/apps/systemtags/src/systemtagsinfoview.js index 2ec1ba0fef..d5b8b2f272 100644 --- a/apps/systemtags/src/systemtagsinfoview.js +++ b/apps/systemtags/src/systemtagsinfoview.js @@ -31,6 +31,7 @@ _rendered: false, className: 'systemTagsInfoView', + name: 'systemTags', /** * @type OC.SystemTags.SystemTagsInputField From a48359ac024b1b0dbfea2dc3deb93607b0dbf329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Wed, 16 Oct 2019 21:46:31 +0200 Subject: [PATCH 09/14] Adjust unit tests to new OCA.Sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- apps/comments/tests/js/filespluginSpec.js | 2 +- apps/files/tests/js/filelistSpec.js | 208 ------------------ apps/files_sharing/tests/js/shareSpec.js | 207 ----------------- .../tests/js/systemtagsinfoviewSpec.js | 6 +- 4 files changed, 4 insertions(+), 419 deletions(-) diff --git a/apps/comments/tests/js/filespluginSpec.js b/apps/comments/tests/js/filespluginSpec.js index 78becc5af0..0613169803 100644 --- a/apps/comments/tests/js/filespluginSpec.js +++ b/apps/comments/tests/js/filespluginSpec.js @@ -74,7 +74,7 @@ describe('OCA.Comments.FilesPlugin tests', function() { expect(sidebarStub.calledOnce).toEqual(true); expect(sidebarStub.lastCall.args[0]).toEqual('One.txt'); - expect(sidebarStub.lastCall.args[1]).toEqual('commentsTabView'); + expect(sidebarStub.lastCall.args[1]).toEqual('comments'); }); }); describe('elementToFile', function() { diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 402cfaa107..f9c1b5f31c 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -748,36 +748,6 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.calledOnce).toEqual(true); }); - it('Shows renamed file details if rename ajax call suceeded', function() { - fileList.showDetailsView('One.txt'); - - expect($('#app-sidebar').hasClass('disappear')).toEqual(false); - expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1); - expect(fileList._detailsView.getFileInfo().get('name')).toEqual('One.txt'); - - doRename(); - - deferredRename.resolve(201); - - expect($('#app-sidebar').hasClass('disappear')).toEqual(false); - expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1); - expect(fileList._detailsView.getFileInfo().get('name')).toEqual('Tu_after_three.txt'); - }); - it('Shows again file details if rename ajax call failed', function() { - fileList.showDetailsView('One.txt'); - - expect($('#app-sidebar').hasClass('disappear')).toEqual(false); - expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1); - expect(fileList._detailsView.getFileInfo().get('name')).toEqual('One.txt'); - - doRename(); - - deferredRename.reject(403); - - expect($('#app-sidebar').hasClass('disappear')).toEqual(false); - expect(fileList._detailsView.getFileInfo().get('id')).toEqual(1); - expect(fileList._detailsView.getFileInfo().get('name')).toEqual('One.txt'); - }); it('Correctly updates file link after rename', function() { var $tr; doRename(); @@ -2460,184 +2430,6 @@ describe('OCA.Files.FileList tests', function() { }); }); }); - describe('Details sidebar', function() { - beforeEach(function() { - fileList.setFiles(testFiles); - fileList.showDetailsView('Two.jpg'); - }); - describe('registering', function() { - var addTabStub; - var addDetailStub; - - beforeEach(function() { - addTabStub = sinon.stub(OCA.Files.DetailsView.prototype, 'addTabView'); - addDetailStub = sinon.stub(OCA.Files.DetailsView.prototype, 'addDetailView'); - getDetailsStub = sinon.stub(OCA.Files.DetailsView.prototype, 'getDetailViews'); - }); - afterEach(function() { - addTabStub.restore(); - addDetailStub.restore(); - getDetailsStub.restore(); - }); - it('forward the registered views to the underlying DetailsView', function() { - fileList.destroy(); - fileList = new OCA.Files.FileList($('#app-content-files'), { - detailsViewEnabled: true - }); - fileList.registerTabView(new OCA.Files.DetailTabView()); - fileList.registerDetailView(new OCA.Files.DetailFileInfoView()); - - expect(addTabStub.calledOnce).toEqual(true); - // twice because the filelist already registers one by default - expect(addDetailStub.calledTwice).toEqual(true); - }); - it('forward getting the registered views to the underlying DetailsView', function() { - fileList.destroy(); - fileList = new OCA.Files.FileList($('#app-content-files'), { - detailsViewEnabled: true - }); - var expectedRegisteredDetailsView = []; - getDetailsStub.returns(expectedRegisteredDetailsView); - - var registeredDetailViews = fileList.getRegisteredDetailViews(); - - expect(getDetailsStub.calledOnce).toEqual(true); - expect(registeredDetailViews).toEqual(expectedRegisteredDetailsView); - }); - it('does not error when registering panels when not details view configured', function() { - fileList.destroy(); - fileList = new OCA.Files.FileList($('#app-content-files'), { - detailsViewEnabled: false - }); - fileList.registerTabView(new OCA.Files.DetailTabView()); - fileList.registerDetailView(new OCA.Files.DetailFileInfoView()); - - expect(addTabStub.notCalled).toEqual(true); - expect(addDetailStub.notCalled).toEqual(true); - }); - it('returns null when getting the registered views when not details view configured', function() { - fileList.destroy(); - fileList = new OCA.Files.FileList($('#app-content-files'), { - detailsViewEnabled: false - }); - - var registeredDetailViews = fileList.getRegisteredDetailViews(); - - expect(getDetailsStub.notCalled).toEqual(true); - expect(registeredDetailViews).toBeNull(); - }); - }); - it('triggers file action when clicking on row if no details view configured', function() { - fileList.destroy(); - fileList = new OCA.Files.FileList($('#app-content-files'), { - detailsViewEnabled: false - }); - var updateDetailsViewStub = sinon.stub(fileList, '_updateDetailsView'); - var actionStub = sinon.stub(); - fileList.setFiles(testFiles); - fileList.fileActions.register( - 'text/plain', - 'Test', - OC.PERMISSION_ALL, - function() { - // Specify icon for hitory button - return OC.imagePath('core','actions/history'); - }, - actionStub - ); - fileList.fileActions.setDefault('text/plain', 'Test'); - var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filesize').click(); - expect(actionStub.calledOnce).toEqual(true); - expect(updateDetailsViewStub.notCalled).toEqual(true); - updateDetailsViewStub.restore(); - }); - it('highlights current file when clicked and updates sidebar', function() { - fileList.fileActions.setDefault('text/plain', 'Test'); - var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filesize').click(); - expect($tr.hasClass('highlighted')).toEqual(true); - - expect(fileList._detailsView.getFileInfo().id).toEqual(1); - }); - it('keeps the last highlighted file when clicking outside', function() { - var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filesize').click(); - - fileList.$el.find('tfoot').click(); - - expect($tr.hasClass('highlighted')).toEqual(true); - expect(fileList._detailsView.getFileInfo().id).toEqual(1); - }); - it('removes last highlighted file when selecting via checkbox', function() { - var $tr = fileList.findFileEl('One.txt'); - - // select - $tr.find('td.filesize').click(); - $tr.find('input:checkbox').click(); - expect($tr.hasClass('highlighted')).toEqual(false); - - // deselect - $tr.find('td.filesize').click(); - $tr.find('input:checkbox').click(); - expect($tr.hasClass('highlighted')).toEqual(false); - - expect(fileList._detailsView.getFileInfo()).toEqual(null); - }); - it('removes last highlighted file when selecting all files via checkbox', function() { - var $tr = fileList.findFileEl('One.txt'); - - // select - $tr.find('td.filesize').click(); - fileList.$el.find('.select-all.checkbox').click(); - expect($tr.hasClass('highlighted')).toEqual(false); - - // deselect - $tr.find('td.filesize').click(); - fileList.$el.find('.select-all.checkbox').click(); - expect($tr.hasClass('highlighted')).toEqual(false); - - expect(fileList._detailsView.getFileInfo()).toEqual(null); - }); - it('closes sidebar whenever the currently highlighted file was removed from the list', function() { - jQuery.fx.off = true; - var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filesize').click(); - expect($tr.hasClass('highlighted')).toEqual(true); - - expect(fileList._detailsView.getFileInfo().id).toEqual(1); - - expect($('#app-sidebar').hasClass('disappear')).toEqual(false); - fileList.remove('One.txt'); - // sidebar is removed on close before being - expect($('#app-sidebar').length).toEqual(0); - jQuery.fx.off = false; - }); - it('returns the currently selected model instance when calling getModelForFile', function() { - var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filesize').click(); - - var model1 = fileList.getModelForFile('One.txt'); - var model2 = fileList.getModelForFile('One.txt'); - model1.set('test', true); - - // it's the same model - expect(model2).toEqual(model1); - - var model3 = fileList.getModelForFile($tr); - expect(model3).toEqual(model1); - }); - it('closes the sidebar when switching folders', function() { - jQuery.fx.off = true; - var $tr = fileList.findFileEl('One.txt'); - $tr.find('td.filesize').click(); - - expect($('#app-sidebar').hasClass('disappear')).toEqual(false); - fileList.changeDirectory('/another'); - expect($('#app-sidebar').length).toEqual(0); - jQuery.fx.off = false; - }); - }); describe('File actions', function() { it('Clicking on a file name will trigger default action', function() { var actionStub = sinon.stub(); diff --git a/apps/files_sharing/tests/js/shareSpec.js b/apps/files_sharing/tests/js/shareSpec.js index a4aa6eea74..061c789c14 100644 --- a/apps/files_sharing/tests/js/shareSpec.js +++ b/apps/files_sharing/tests/js/shareSpec.js @@ -234,198 +234,6 @@ describe('OCA.Sharing.Util tests', function() { expect($tr.find('.action-share').length).toEqual(0); }); }); - describe('Share action', function() { - var shareTab; - - function makeDummyShareItem(displayName) { - return { - share_with_displayname: displayName - }; - } - - beforeEach(function() { - // make it look like not the "All files" list - fileList.id = 'test'; - shareTab = fileList._detailsView._tabViews[0]; - }); - afterEach(function() { - shareTab = null; - }); - it('clicking share action opens sidebar and share tab', function() { - var showDetailsViewStub = sinon.stub(fileList, 'showDetailsView'); - - fileList.setFiles([{ - id: 1, - type: 'file', - name: 'One.txt', - path: '/subdir', - mimetype: 'text/plain', - size: 12, - permissions: OC.PERMISSION_ALL, - etag: 'abc' - }]); - - var $tr = fileList.$el.find('tr:first'); - $tr.find('.action-share').click(); - - expect(showDetailsViewStub.calledOnce).toEqual(true); - expect(showDetailsViewStub.getCall(0).args[0]).toEqual('One.txt'); - expect(showDetailsViewStub.getCall(0).args[1]).toEqual('shareTabView'); - - showDetailsViewStub.restore(); - }); - it('adds share icon after sharing a non-shared file', function() { - var $action, $tr; - OC.Share.statuses = {}; - fileList.setFiles([{ - id: 1, - type: 'file', - name: 'One.txt', - path: '/subdir', - mimetype: 'text/plain', - size: 12, - permissions: OC.PERMISSION_ALL, - etag: 'abc' - }]); - $action = fileList.$el.find('tbody tr:first .action-share'); - $tr = fileList.$el.find('tr:first'); - - $tr.find('.action-share').click(); - - // simulate updating shares - shareTab._dialog.model.set({ - shares: [ - {share_with_displayname: 'User One', share_with: 'User One'}, - {share_with_displayname: 'User Two', share_with: 'User Two'}, - {share_with_displayname: 'Group One', share_with: 'Group One'}, - {share_with_displayname: 'Group Two', share_with: 'Group Two'} - ] - }); - - expect($action.text().trim()).toEqual('Shared with Group One Shared with Group Two Shared with User One Shared with User Two'); - expect($action.find('.icon').hasClass('icon-shared')).toEqual(true); - expect($action.find('.icon').hasClass('icon-public')).toEqual(false); - }); - it('updates share icon after updating shares of a file', function() { - var $action, $tr; - OC.Share.statuses = {1: {link: false, path: '/subdir'}}; - fileList.setFiles([{ - id: 1, - type: 'file', - name: 'One.txt', - path: '/subdir', - mimetype: 'text/plain', - size: 12, - permissions: OC.PERMISSION_ALL, - etag: 'abc' - }]); - $action = fileList.$el.find('tbody tr:first .action-share'); - $tr = fileList.$el.find('tr:first'); - - $tr.find('.action-share').click(); - - // simulate updating shares - shareTab._dialog.model.set({ - shares: [ - {share_with_displayname: 'User One', share_with: 'User One'}, - {share_with_displayname: 'User Two', share_with: 'User Two'}, - {share_with_displayname: 'User Three', share_with: 'User Three'} - ] - }); - - expect($action.text().trim()).toEqual('Shared with User One Shared with User Three Shared with User Two'); - expect($action.find('.icon').hasClass('icon-shared')).toEqual(true); - expect($action.find('.icon').hasClass('icon-public')).toEqual(false); - }); - it('removes share icon after removing all shares from a file', function() { - var $action, $tr; - OC.Share.statuses = {1: {link: false, path: '/subdir'}}; - fileList.setFiles([{ - id: 1, - type: 'file', - name: 'One.txt', - path: '/subdir', - mimetype: 'text/plain', - size: 12, - permissions: OC.PERMISSION_ALL, - etag: 'abc', - recipients: 'User One, User Two' - }]); - $action = fileList.$el.find('tbody tr:first .action-share'); - $tr = fileList.$el.find('tr:first'); - - $tr.find('.action-share').click(); - - // simulate updating shares - shareTab._dialog.model.set({ - shares: [] - }); - - expect($tr.attr('data-share-recipient-data')).not.toBeDefined(); - }); - it('keep share text after updating reshare', function() { - var $action, $tr; - OC.Share.statuses = {1: {link: false, path: '/subdir'}}; - fileList.setFiles([{ - id: 1, - type: 'file', - name: 'One.txt', - path: '/subdir', - mimetype: 'text/plain', - size: 12, - permissions: OC.PERMISSION_ALL, - etag: 'abc', - shareOwner: 'User One', - shareOwnerId: 'User One' - }]); - $action = fileList.$el.find('tbody tr:first .action-share'); - $tr = fileList.$el.find('tr:first'); - - $tr.find('.action-share').click(); - - // simulate updating shares - shareTab._dialog.model.set({ - shares: [{share_with_displayname: 'User Two'}] - }); - - expect($action.find('>span').text().trim()).toEqual('Shared by User One'); - expect($action.find('.icon').hasClass('icon-shared')).toEqual(false); - expect($action.find('.icon').hasClass('icon-public')).toEqual(false); - }); - it('keep share text after unsharing reshare', function() { - var $action, $tr; - OC.Share.statuses = {1: {link: false, path: '/subdir'}}; - fileList.setFiles([{ - id: 1, - type: 'file', - name: 'One.txt', - path: '/subdir', - mimetype: 'text/plain', - size: 12, - permissions: OC.PERMISSION_ALL, - etag: 'abc', - shareOwner: 'User One', - shareOwnerId: 'User One', - recipients: 'User Two', - recipientData: {'User Two': 'User Two'} - }]); - $action = fileList.$el.find('tbody tr:first .action-share'); - $tr = fileList.$el.find('tr:first'); - - $tr.find('.action-share').click(); - - // simulate updating shares - shareTab._dialog.model.set({ - shares: [] - }); - - expect($tr.attr('data-share-recipient-data')).not.toBeDefined(); - - expect($action.find('>span').text().trim()).toEqual('Shared by User One'); - expect($action.find('.icon').hasClass('icon-shared')).toEqual(false); - expect($action.find('.icon').hasClass('icon-public')).toEqual(false); - }); - }); describe('Excluded lists', function() { function createListThenAttach(listId) { var fileActions = new OCA.Files.FileActions(); @@ -513,20 +321,5 @@ describe('OCA.Sharing.Util tests', function() { afterEach(function() { shareTabSpy.restore(); }); - - it('updates fileInfoModel when shares changed', function() { - var changeHandler = sinon.stub(); - fileInfoModel.on('change', changeHandler); - - shareTabSpy.getCall(0).returnValue.trigger('sharesChanged', shareModel); - - expect(changeHandler.calledOnce).toEqual(true); - expect(changeHandler.getCall(0).args[0].changed).toEqual({ - shareTypes: [ - OC.Share.SHARE_TYPE_USER, - OC.Share.SHARE_TYPE_REMOTE - ] - }); - }); }); }); diff --git a/apps/systemtags/tests/js/systemtagsinfoviewSpec.js b/apps/systemtags/tests/js/systemtagsinfoviewSpec.js index 2f87468811..99c0471d50 100644 --- a/apps/systemtags/tests/js/systemtagsinfoviewSpec.js +++ b/apps/systemtags/tests/js/systemtagsinfoviewSpec.js @@ -45,7 +45,7 @@ describe('OCA.SystemTags.SystemTagsInfoView tests', function() { var fetchStub = sinon.stub(OC.SystemTags.SystemTagsMappingCollection.prototype, 'fetch'); var setDataStub = sinon.stub(OC.SystemTags.SystemTagsInputField.prototype, 'setData'); - expect(view.$el.hasClass('hidden')).toEqual(true); + expect(view.$el.hasClass('hidden')).toEqual(false); view.setFileInfo({id: '123'}); expect(view.$el.find('input[name=tags]').length).toEqual(1); @@ -211,10 +211,10 @@ describe('OCA.SystemTags.SystemTagsInfoView tests', function() { expect(view.isVisible()).toBeTruthy(); }); - it('is not visible after rendering', function() { + it('is visible after rendering', function() { view.render(); - expect(view.isVisible()).toBeFalsy(); + expect(view.isVisible()).toBeTruthy(); }); it('shows and hides the element', function() { view.show(); From 12eba18bdfc52b49dcc7c0a8eb2bd97dd6fddc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Wed, 16 Oct 2019 21:46:31 +0200 Subject: [PATCH 10/14] Adjust acceptance tests to new OCA.Sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .../features/app-files-sharing-link.feature | 3 +- .../features/app-files-tags.feature | 9 --- .../features/bootstrap/FilesAppContext.php | 12 +-- .../bootstrap/FilesAppSharingContext.php | 75 ++++++++++++------- 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/tests/acceptance/features/app-files-sharing-link.feature b/tests/acceptance/features/app-files-sharing-link.feature index c35338fea2..38680f110d 100644 --- a/tests/acceptance/features/app-files-sharing-link.feature +++ b/tests/acceptance/features/app-files-sharing-link.feature @@ -126,8 +126,7 @@ Feature: app-files-sharing-link Given I am logged in And I share the link for "welcome.txt" When I protect the shared link with the password "abcdef" - Then I see that the working icon for password protect is shown - And I see that the working icon for password protect is eventually not shown + Then I see that the password protect is disabled while loading And I see that the link share is password protected # As Talk is not enabled in the acceptance tests of the server the checkbox # is never shown. diff --git a/tests/acceptance/features/app-files-tags.feature b/tests/acceptance/features/app-files-tags.feature index 5c0942472a..093ae37191 100644 --- a/tests/acceptance/features/app-files-tags.feature +++ b/tests/acceptance/features/app-files-tags.feature @@ -19,15 +19,6 @@ Feature: app-files-tags # When I open the input field for tags in the details view # Then I see that the input field for tags in the details view is shown - Scenario: show the input field for tags in the details view after the sharing tab has loaded - Given I am logged in - And I open the details view for "welcome.txt" - And I see that the details view is open - And I open the "Sharing" tab in the details view - And I see that the "Sharing" tab in the details view is eventually loaded - When I open the input field for tags in the details view - Then I see that the input field for tags in the details view is shown - Scenario: create tags using the Administration settings Given I am logged in as the admin And I visit the settings page diff --git a/tests/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php index 880303fe1d..32b0191618 100644 --- a/tests/acceptance/features/bootstrap/FilesAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppContext.php @@ -98,7 +98,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function fileNameInDetailsView() { - return Locator::forThe()->css(".fileName")-> + return Locator::forThe()->css(".app-sidebar-header__title")-> descendantOf(self::detailsView())-> describedAs("File name in details view in Files app"); } @@ -107,7 +107,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function favoriteActionInFileDetailsInDetailsView() { - return Locator::forThe()->css(".action-favorite")-> + return Locator::forThe()->css(".app-sidebar-header__star")-> descendantOf(self::fileDetailsInDetailsView())-> describedAs("Favorite action in file details in details view in Files app"); } @@ -143,7 +143,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ private static function fileDetailsInDetailsView() { - return Locator::forThe()->css(".file-details")-> + return Locator::forThe()->css(".app-sidebar-header__desc")-> descendantOf(self::detailsView())-> describedAs("File details in details view in Files app"); } @@ -205,7 +205,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ private static function tabHeadersInDetailsView() { - return Locator::forThe()->css(".tabHeaders")-> + return Locator::forThe()->css(".app-sidebar-tabs__nav")-> descendantOf(self::detailsView())-> describedAs("Tab headers in details view in Files app"); } @@ -214,7 +214,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function tabInDetailsViewNamed($tabName) { - return Locator::forThe()->xpath("//div[@id=//*[contains(concat(' ', normalize-space(@class), ' '), ' tabHeader ') and normalize-space() = '$tabName']/@data-tabid]")-> + return Locator::forThe()->xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' app-sidebar-tabs__content ')]/section[@aria-labelledby = '$tabName' and @role = 'tabpanel']")-> descendantOf(self::detailsView())-> describedAs("Tab named $tabName in details view in Files app"); } @@ -223,7 +223,7 @@ class FilesAppContext implements Context, ActorAwareInterface { * @return Locator */ public static function loadingIconForTabInDetailsViewNamed($tabName) { - return Locator::forThe()->css(".loading")-> + return Locator::forThe()->css(".icon-loading")-> descendantOf(self::tabInDetailsViewNamed($tabName))-> describedAs("Loading icon for tab named $tabName in details view in Files app"); } diff --git a/tests/acceptance/features/bootstrap/FilesAppSharingContext.php b/tests/acceptance/features/bootstrap/FilesAppSharingContext.php index 5353f05c11..6bebfc5b3d 100644 --- a/tests/acceptance/features/bootstrap/FilesAppSharingContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppSharingContext.php @@ -31,7 +31,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function sharedByLabel() { - return Locator::forThe()->css(".reshare")-> + return Locator::forThe()->css(".sharing-entry__reshare")-> descendantOf(FilesAppContext::detailsView())-> describedAs("Shared by label in the details view in Files app"); } @@ -40,16 +40,34 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function shareWithInput() { - return Locator::forThe()->css(".shareWithField")-> + return Locator::forThe()->css(".sharing-input .multiselect__input")-> descendantOf(FilesAppContext::detailsView())-> describedAs("Share with input in the details view in Files app"); } + /** + * @return Locator + */ + public static function shareWithInputResults() { + return Locator::forThe()->css(".sharing-input .multiselect__content-wrapper")-> + descendantOf(FilesAppContext::detailsView())-> + describedAs("Share with input results list in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function shareWithInputResult($result) { + return Locator::forThe()->xpath("//li[contains(concat(' ', normalize-space(@class), ' '), ' multiselect__element ')]//span[normalize-space() = '$result']/ancestor::li")-> + descendantOf(self::shareWithInputResults())-> + describedAs("Share with input result from the results list in the details view in Files app"); + } + /** * @return Locator */ public static function shareeList() { - return Locator::forThe()->css(".shareeListView")-> + return Locator::forThe()->css(".sharing-sharee-list")-> descendantOf(FilesAppContext::detailsView())-> describedAs("Sharee list in the details view in Files app"); } @@ -60,7 +78,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { public static function sharedWithRow($sharedWithName) { // "username" class is used for any type of share, not only for shares // with users. - return Locator::forThe()->xpath("//span[contains(concat(' ', normalize-space(@class), ' '), ' username ') and normalize-space() = '$sharedWithName']/ancestor::li")-> + return Locator::forThe()->xpath("//li[contains(concat(' ', normalize-space(@class), ' '), ' sharing-entry ')]//h5[normalize-space() = '$sharedWithName']/ancestor::li")-> descendantOf(self::shareeList())-> describedAs("Shared with $sharedWithName row in the details view in Files app"); } @@ -69,7 +87,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function shareWithMenuButton($sharedWithName) { - return Locator::forThe()->css(".share-menu > .icon")-> + return Locator::forThe()->css(".sharing-entry__actions > .action-item__menutoggle")-> descendantOf(self::sharedWithRow($sharedWithName))-> describedAs("Share with $sharedWithName menu button in the details view in Files app"); } @@ -78,7 +96,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function shareWithMenu($sharedWithName) { - return Locator::forThe()->css(".share-menu > .menu")-> + return Locator::forThe()->css(".sharing-entry__actions > .action-item__menu")-> descendantOf(self::sharedWithRow($sharedWithName))-> describedAs("Share with $sharedWithName menu in the details view in Files app"); } @@ -108,7 +126,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function shareLinkRow() { - return Locator::forThe()->css(".linkShareView .shareWithList:first-child")-> + return Locator::forThe()->css(".sharing-link-list .sharing-entry__link:first-child")-> descendantOf(FilesAppContext::detailsView())-> describedAs("Share link row in the details view in Files app"); } @@ -119,7 +137,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { public static function shareLinkAddNewButton() { // When there is no link share the "Add new share" item is shown instead // of the menu button as a direct child of ".share-menu". - return Locator::forThe()->css(".share-menu > .new-share")-> + return Locator::forThe()->css(".action-item.icon-add")-> descendantOf(self::shareLinkRow())-> describedAs("Add new share link button in the details view in Files app"); } @@ -128,7 +146,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function copyLinkButton() { - return Locator::forThe()->css("a.clipboard-button")-> + return Locator::forThe()->css("a.sharing-entry__copy")-> descendantOf(self::shareLinkRow())-> describedAs("Copy link button in the details view in Files app"); } @@ -137,7 +155,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function shareLinkMenuButton() { - return Locator::forThe()->css(".share-menu > .icon")-> + return Locator::forThe()->css(".sharing-entry__actions .action-item__menutoggle")-> descendantOf(self::shareLinkRow())-> describedAs("Share link menu button in the details view in Files app"); } @@ -146,7 +164,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function shareLinkMenu() { - return Locator::forThe()->css(".share-menu > .menu")-> + return Locator::forThe()->css(".sharing-entry__actions .action-item__menu")-> descendantOf(self::shareLinkRow())-> describedAs("Share link menu in the details view in Files app"); } @@ -209,16 +227,16 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { * @return Locator */ public static function passwordProtectField() { - return Locator::forThe()->css(".linkPassText")->descendantOf(self::shareLinkMenu())-> + return Locator::forThe()->css(".share-link-password input.action-input__input")->descendantOf(self::shareLinkMenu())-> describedAs("Password protect field in the details view in Files app"); } /** * @return Locator */ - public static function passwordProtectWorkingIcon() { - return Locator::forThe()->css(".linkPassMenu .icon-loading-small")->descendantOf(self::shareLinkMenu())-> - describedAs("Password protect working icon in the details view in Files app"); + public static function disabledPasswordProtectField() { + return Locator::forThe()->css(".share-link-password input.action-input__input[disabled]")->descendantOf(self::shareLinkMenu())-> + describedAs("Disabled password protect field in the details view in Files app"); } /** @@ -257,7 +275,12 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { public function iShareWith($fileName, $shareWithName) { $this->actor->find(FileListContext::shareActionForFile(FilesAppContext::currentSectionMainView(), $fileName), 10)->click(); - $this->actor->find(self::shareWithInput(), 5)->setValue($shareWithName . "\r"); + $this->actor->find(self::shareWithInput(), 5)->setValue($shareWithName); + // "setValue()" ends sending a tab, which unfocuses the input and causes + // the results to be hidden, so the input needs to be clicked to show + // the results again. + $this->actor->find(self::shareWithInput())->click(); + $this->actor->find(self::shareWithInputResult($shareWithName), 5)->click(); } /** @@ -269,7 +292,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { // 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::copyLinkButton(), 2)->getWrappedElement()->getAttribute("data-clipboard-text"); + $this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::copyLinkButton(), 2)->getWrappedElement()->getAttribute("href"); } /** @@ -412,21 +435,16 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { } /** - * @Then I see that the working icon for password protect is shown + * @Then I see that the password protect is disabled while loading */ - public function iSeeThatTheWorkingIconForPasswordProtectIsShown() { - PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::passwordProtectWorkingIcon(), 10)); - } + public function iSeeThatThePasswordProtectIsDisabledWhileLoading() { + PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::disabledPasswordProtectField(), 10)); - /** - * @Then I see that the working icon for password protect is eventually not shown - */ - public function iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown() { if (!WaitFor::elementToBeEventuallyNotShown( $this->actor, - self::passwordProtectWorkingIcon(), + self::disabledPasswordProtectField(), $timeout = 10 * $this->actor->getFindTimeoutMultiplier())) { - PHPUnit_Framework_Assert::fail("The working icon for password protect is still shown after $timeout seconds"); + PHPUnit_Framework_Assert::fail("The password protect field is still disabled after $timeout seconds"); } } @@ -477,8 +495,7 @@ class FilesAppSharingContext implements Context, ActorAwareInterface { public function iShareTheLinkForProtectedByThePassword($fileName, $password) { $this->iShareTheLinkFor($fileName); $this->iProtectTheSharedLinkWithThePassword($password); - $this->iSeeThatTheWorkingIconForPasswordProtectIsShown(); - $this->iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown(); + $this->iSeeThatThePasswordProtectIsDisabledWhileLoading(); } private function showShareLinkMenuIfNeeded() { From 51960cb228760d79ed70996a7a87f8719af8230c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Wed, 23 Oct 2019 11:41:47 +0200 Subject: [PATCH 11/14] Fix triggering favorite file actions on the file list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- apps/files/src/views/Sidebar.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index e99389de75..879f71d8a1 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -315,6 +315,13 @@ export default { ${state ? '' : ''} ` }) + + // TODO: Obliterate as soon as possible and use events with new files app + // Terrible fallback for legacy files: toggle filelist as well + if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) { + OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList) + } + } catch (error) { OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file')) console.error('Unable to change favourite state', error) From 3331cdd74abb89692c9d37ba503a9d36768ae7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Wed, 23 Oct 2019 18:40:48 +0200 Subject: [PATCH 12/14] Fix legacy tab backbone fileinfo change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- apps/files/src/components/LegacyTab.vue | 7 ++++++- apps/files/src/views/Sidebar.vue | 20 +++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/files/src/components/LegacyTab.vue b/apps/files/src/components/LegacyTab.vue index 9a85ee7f07..54a24edcdd 100644 --- a/apps/files/src/components/LegacyTab.vue +++ b/apps/files/src/components/LegacyTab.vue @@ -40,7 +40,6 @@ export default { }, name: { type: String, - default: '', required: true }, fileInfo: { @@ -74,10 +73,16 @@ export default { } } }, + beforeMount() { + this.setFileInfo(this.fileInfo) + }, mounted() { // append the backbone element and set the FileInfo this.component.$el.appendTo(this.$el) }, + beforeDestroy() { + this.component.remove() + }, methods: { setFileInfo(fileInfo) { this.component.setFileInfo(new OCA.Files.FileInfoModel(fileInfo)) diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index 879f71d8a1..02913d3687 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -26,6 +26,7 @@ ref="sidebar" v-bind="appSidebar" @close="onClose" + @update:active="setActiveTab" @update:starred="toggleStarred" @[defaultActionListener].stop.prevent="onDefaultAction"> @@ -50,6 +51,7 @@ :key="tab.id" :component="tabComponent(tab).component" :name="tab.name" + :dav-path="davPath" :file-info="fileInfo" /> @@ -121,13 +123,8 @@ export default { * @param {string} id the tab id to set as active * @returns {string} the current active tab */ - activeTab: { - get: function() { - return this.Sidebar.activeTab - }, - set: function(id) { - OCA.Files.Sidebar.activeTab = id - } + activeTab() { + return this.Sidebar.activeTab }, /** @@ -294,6 +291,15 @@ export default { } }, + /** + * Set current active tab + * + * @param {string} id tab unique id + */ + setActiveTab(id) { + OCA.Files.Sidebar.activeTab = id + }, + /** * Toggle favourite state * TODO: better implementation From 2fd057513a4eafd1282128721fb6dbc443ffdeba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Thu, 24 Oct 2019 15:51:56 +0200 Subject: [PATCH 13/14] Fix current user edit/delete permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .../lib/Controller/ShareAPIController.php | 4 + .../src/components/SharingEntry.vue | 127 +++++++++--------- .../src/components/SharingEntryLink.vue | 7 +- apps/files_sharing/src/models/Share.js | 25 ++++ .../Controller/ShareAPIControllerTest.php | 81 +++++++++++ 5 files changed, 181 insertions(+), 63 deletions(-) diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index e44ca84a09..66b2383ea7 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -154,7 +154,11 @@ class ShareAPIController extends OCSController { 'share_type' => $share->getShareType(), 'uid_owner' => $share->getSharedBy(), 'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(), + // recipient permissions 'permissions' => $share->getPermissions(), + // current user permissions on this share + 'can_edit' => $this->canEditShare($share), + 'can_delete' => $this->canDeleteShare($share), 'stime' => $share->getShareTime()->getTimestamp(), 'parent' => null, 'expiration' => null, diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue index 857b57adbd..09d09d607f 100644 --- a/apps/files_sharing/src/components/SharingEntry.vue +++ b/apps/files_sharing/src/components/SharingEntry.vue @@ -30,75 +30,80 @@
{{ title }}