Compare commits
79 Commits
availabili
...
v20.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
615b994816 | ||
|
|
c2eb39d662 | ||
|
|
d2bd0664be | ||
|
|
d3d7209be9 | ||
|
|
2c149fbd9a | ||
|
|
998ab15206 | ||
|
|
d204e5855c | ||
|
|
bb662c20fe | ||
|
|
097d62049d | ||
|
|
be778c94e1 | ||
|
|
ac3a32f305 | ||
|
|
28ae039588 | ||
|
|
1f0a5aeae3 | ||
|
|
741ebf5177 | ||
|
|
4817005733 | ||
|
|
fa4cd4435b | ||
|
|
61a0069dd5 | ||
|
|
2433550eff | ||
|
|
d7d805ef79 | ||
|
|
612306d290 | ||
|
|
b66f5c55e5 | ||
|
|
413a6042f3 | ||
|
|
f0dc0d1347 | ||
|
|
6b8356ce35 | ||
|
|
143c6356c5 | ||
|
|
2e87668f77 | ||
|
|
2364808913 | ||
|
|
f72ebcd956 | ||
|
|
b1879c4fcb | ||
|
|
05fa5e4d9e | ||
|
|
851333edab | ||
|
|
71b33fb87a | ||
|
|
6365e7e162 | ||
|
|
fb426c90b7 | ||
|
|
15ff980583 | ||
|
|
d358f9dddf | ||
|
|
7496a10227 | ||
|
|
7630052a60 | ||
|
|
5ab7392d56 | ||
|
|
aefbf4c01d | ||
|
|
d81b4e2ff7 | ||
|
|
3320d8ecf1 | ||
|
|
a1d3213e7d | ||
|
|
5475bb4083 | ||
|
|
87f8e1e366 | ||
|
|
0dd18e0356 | ||
|
|
bf9a24efbe | ||
|
|
87b9dbdb56 | ||
|
|
19390a4b5e | ||
|
|
99b25ef3fe | ||
|
|
d247f198a9 | ||
|
|
e29b5c6d92 | ||
|
|
fa0f815dda | ||
|
|
2be71de5a1 | ||
|
|
724276c7a7 | ||
|
|
26603c7cdd | ||
|
|
7edced3807 | ||
|
|
3daddfce14 | ||
|
|
9838d54cb5 | ||
|
|
46babff37b | ||
|
|
ff8eb8dfa2 | ||
|
|
289bc8e345 | ||
|
|
a0e8a78945 | ||
|
|
065f3e125e | ||
|
|
bf58dcb247 | ||
|
|
ada7ad6930 | ||
|
|
5d81cb36b5 | ||
|
|
03d00afe31 | ||
|
|
b35daf665f | ||
|
|
55393939ce | ||
|
|
e9e5a02d7c | ||
|
|
7ad973494a | ||
|
|
5646144fae | ||
|
|
839f597921 | ||
|
|
3c6319f275 | ||
|
|
2672f5da59 | ||
|
|
2000e2faa5 | ||
|
|
0504873a8a | ||
|
|
eba4723428 |
@@ -1350,7 +1350,7 @@ steps:
|
||||
commands:
|
||||
# JavaScript files are not used in integration tests so it is not needed to
|
||||
# build them.
|
||||
- git clone --depth 1 https://github.com/nextcloud/spreed apps/spreed
|
||||
- git clone --branch stable20 --depth 1 https://github.com/nextcloud/spreed apps/spreed
|
||||
- name: integration-sharing-v1-video-verification
|
||||
image: nextcloudci/integration-php7.3:integration-php7.3-2
|
||||
commands:
|
||||
|
||||
18
.gitattributes
vendored
18
.gitattributes
vendored
@@ -1,8 +1,8 @@
|
||||
/core/js/dist/*.js binary
|
||||
/core/js/dist/*.js.map binary
|
||||
|
||||
/apps/accessibility/js/accessibility.js binary
|
||||
/apps/accessibility/js/accessibility.js.map binary
|
||||
/apps/accessibility/js/*.js binary
|
||||
/apps/accessibility/js/*.js.map binary
|
||||
/apps/comments/js/*.js binary
|
||||
/apps/comments/js/*.js.map binary
|
||||
/apps/dashboard/js/*.js binary
|
||||
@@ -11,10 +11,12 @@
|
||||
/apps/files/js/dist/*.js.map binary
|
||||
/apps/files_sharing/js/dist/*.js binary
|
||||
/apps/files_sharing/js/dist/*.js.map binary
|
||||
/apps/files_versions/js/files_versions.js binary
|
||||
/apps/files_versions/js/files_versions.js.map binary
|
||||
/apps/oauth2/js/oauth2.js binary
|
||||
/apps/oauth2/js/oauth2.js.map binary
|
||||
/apps/files_trashbin/js/*.js binary
|
||||
/apps/files_trashbin/js/*.js.map binary
|
||||
/apps/files_versions/js/*.js binary
|
||||
/apps/files_versions/js/*.js.map binary
|
||||
/apps/oauth2/js/*.js binary
|
||||
/apps/oauth2/js/*.js.map binary
|
||||
/apps/settings/js/vue* binary
|
||||
/apps/systemtags/js/systemtags.js binary
|
||||
/apps/systemtags/js/systemtags.js.map binary
|
||||
@@ -22,5 +24,9 @@
|
||||
/apps/twofactor_backupcodes/js/*.js.map binary
|
||||
/apps/updatenotification/js/updatenotification.js binary
|
||||
/apps/updatenotification/js/updatenotification.js.map binary
|
||||
/apps/user_status/js/*.js binary
|
||||
/apps/user_status/js/*.js.map binary
|
||||
/apps/weather_status/js/*.js binary
|
||||
/apps/weather_status/js/*.js.map binary
|
||||
/apps/workflowengine/js/*.js binary
|
||||
/apps/workflowengine/js/*.js.map binary
|
||||
|
||||
48
.tx/config
48
.tx/config
@@ -133,3 +133,51 @@ file_filter = translationfiles/<lang>/accessibility.po
|
||||
source_file = translationfiles/templates/accessibility.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.provisioning_api]
|
||||
file_filter = translationfiles/<lang>/provisioning_api.po
|
||||
source_file = translationfiles/templates/provisioning_api.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.lookup_server_connector]
|
||||
file_filter = translationfiles/<lang>/lookup_server_connector.po
|
||||
source_file = translationfiles/templates/lookup_server_connector.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.dashboard-shipped-with-server]
|
||||
file_filter = translationfiles/<lang>/dashboard.po
|
||||
source_file = translationfiles/templates/dashboard.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.contactsinteraction]
|
||||
file_filter = translationfiles/<lang>/contactsinteraction.po
|
||||
source_file = translationfiles/templates/contactsinteraction.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.cloud_federation_api]
|
||||
file_filter = translationfiles/<lang>/cloud_federation_api.po
|
||||
source_file = translationfiles/templates/cloud_federation_api.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.admin_audit]
|
||||
file_filter = translationfiles/<lang>/admin_audit.po
|
||||
source_file = translationfiles/templates/admin_audit.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.user_status]
|
||||
file_filter = translationfiles/<lang>/user_status.po
|
||||
source_file = translationfiles/templates/user_status.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[nextcloud.weather_status]
|
||||
file_filter = translationfiles/<lang>/weather_status.po
|
||||
source_file = translationfiles/templates/weather_status.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
0
apps/admin_audit/l10n/.gitkeep
Normal file
0
apps/admin_audit/l10n/.gitkeep
Normal file
0
apps/cloud_federation_api/l10n/.gitkeep
Normal file
0
apps/cloud_federation_api/l10n/.gitkeep
Normal file
0
apps/contactsinteraction/l10n/.gitkeep
Normal file
0
apps/contactsinteraction/l10n/.gitkeep
Normal file
@@ -1,3 +1,12 @@
|
||||
// Suppress "Skip to navigation of app" link since the app does not have a navigation
|
||||
.skip-navigation:not(.skip-content) {
|
||||
display: none;
|
||||
}
|
||||
// Fix position of "Skip to main content" link since the other link is gone
|
||||
.skip-navigation.skip-content {
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
#header {
|
||||
background: transparent !important;
|
||||
--color-header: rgba(24, 24, 24, 1);
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
0
apps/dashboard/l10n/.gitkeep
Normal file
0
apps/dashboard/l10n/.gitkeep
Normal file
@@ -215,6 +215,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.updateGlobalStyles()
|
||||
this.updateSkipLink()
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
|
||||
setInterval(() => {
|
||||
@@ -321,6 +322,10 @@ export default {
|
||||
document.body.classList.remove('dashboard--dark')
|
||||
}
|
||||
},
|
||||
updateSkipLink() {
|
||||
// Make sure "Skip to main content" link points to the app content
|
||||
document.getElementsByClassName('skip-navigation')[0].setAttribute('href', '#app-dashboard')
|
||||
},
|
||||
updateStatusCheckbox(app, checked) {
|
||||
if (checked) {
|
||||
this.enableStatus(app)
|
||||
|
||||
@@ -322,7 +322,7 @@ class SyncService {
|
||||
|
||||
public function syncInstance(\Closure $progressCallback = null) {
|
||||
$systemAddressBook = $this->getLocalSystemAddressBook();
|
||||
$this->userManager->callForSeenUsers(function ($user) use ($systemAddressBook, $progressCallback) {
|
||||
$this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback) {
|
||||
$this->updateUser($user);
|
||||
if (!is_null($progressCallback)) {
|
||||
$progressCallback();
|
||||
|
||||
@@ -38,4 +38,16 @@ class CachingTree extends Tree {
|
||||
}
|
||||
$this->cache[trim($path, '/')] = $node;
|
||||
}
|
||||
|
||||
public function markDirty($path) {
|
||||
// We don't care enough about sub-paths
|
||||
// flushing the entire cache
|
||||
$path = trim($path, '/');
|
||||
foreach ($this->cache as $nodePath => $node) {
|
||||
$nodePath = (string) $nodePath;
|
||||
if ('' === $path || $nodePath == $path || 0 === strpos($nodePath, $path.'/')) {
|
||||
unset($this->cache[$nodePath]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
apps/files/js/dist/personal-settings.js
vendored
2
apps/files/js/dist/personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
apps/files/js/dist/personal-settings.js.map
vendored
2
apps/files/js/dist/personal-settings.js.map
vendored
File diff suppressed because one or more lines are too long
2
apps/files/js/dist/sidebar.js
vendored
2
apps/files/js/dist/sidebar.js
vendored
File diff suppressed because one or more lines are too long
2
apps/files/js/dist/sidebar.js.map
vendored
2
apps/files/js/dist/sidebar.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -44,6 +44,7 @@ use Icewind\SMB\Exception\Exception;
|
||||
use Icewind\SMB\Exception\ForbiddenException;
|
||||
use Icewind\SMB\Exception\InvalidArgumentException;
|
||||
use Icewind\SMB\Exception\NotFoundException;
|
||||
use Icewind\SMB\Exception\OutOfSpaceException;
|
||||
use Icewind\SMB\Exception\TimedOutException;
|
||||
use Icewind\SMB\IFileInfo;
|
||||
use Icewind\SMB\Native\NativeServer;
|
||||
@@ -57,6 +58,7 @@ use OC\Files\Filesystem;
|
||||
use OC\Files\Storage\Common;
|
||||
use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
|
||||
use OCP\Constants;
|
||||
use OCP\Files\EntityTooLargeException;
|
||||
use OCP\Files\Notify\IChange;
|
||||
use OCP\Files\Notify\IRenameChange;
|
||||
use OCP\Files\Storage\INotifyStorage;
|
||||
@@ -497,6 +499,8 @@ class SMB extends Common implements INotifyStorage {
|
||||
return false;
|
||||
} catch (ForbiddenException $e) {
|
||||
return false;
|
||||
} catch (OutOfSpaceException $e) {
|
||||
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
|
||||
} catch (ConnectException $e) {
|
||||
$this->logger->logException($e, ['message' => 'Error while opening file']);
|
||||
throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
|
||||
@@ -538,6 +542,8 @@ class SMB extends Common implements INotifyStorage {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (OutOfSpaceException $e) {
|
||||
throw new EntityTooLargeException("not enough available space to create file", 0, $e);
|
||||
} catch (ConnectException $e) {
|
||||
$this->logger->logException($e, ['message' => 'Error while creating file']);
|
||||
throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
apps/files_sharing/js/dist/collaboration.js
vendored
2
apps/files_sharing/js/dist/collaboration.js
vendored
@@ -1,2 +1,2 @@
|
||||
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=151)}({151:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
|
||||
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=152)}({152:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
|
||||
//# sourceMappingURL=collaboration.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -708,7 +708,7 @@ export default {
|
||||
// Execute the copy link method
|
||||
// freshly created share component
|
||||
// ! somehow does not works on firefox !
|
||||
if (update || !this.config.enforcePasswordForPublicLink) {
|
||||
if (!this.config.enforcePasswordForPublicLink) {
|
||||
// Only copy the link when the password was not forced,
|
||||
// otherwise the user needs to copy/paste the password before finishing the share.
|
||||
component.copyLink()
|
||||
|
||||
0
apps/lookup_server_connector/l10n/.gitkeep
Normal file
0
apps/lookup_server_connector/l10n/.gitkeep
Normal file
0
apps/provisioning_api/l10n/.gitkeep
Normal file
0
apps/provisioning_api/l10n/.gitkeep
Normal file
@@ -1443,6 +1443,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
|
||||
$grid-row-height: 60px;
|
||||
$grid-col-min-width: 160px;
|
||||
overflow-x: scroll;
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
|
||||
#app-content.user-list-grid {
|
||||
display: grid;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -426,8 +426,8 @@ export default {
|
||||
/**
|
||||
* Register search
|
||||
*/
|
||||
subscribe('nextcloud:unified-search:search', this.search)
|
||||
subscribe('nextcloud:unified-search:reset', this.resetSearch)
|
||||
subscribe('nextcloud:unified-search.search', this.search)
|
||||
subscribe('nextcloud:unified-search.reset', this.resetSearch)
|
||||
|
||||
/**
|
||||
* If disabled group but empty, redirect
|
||||
@@ -435,8 +435,8 @@ export default {
|
||||
this.redirectIfDisabled()
|
||||
},
|
||||
beforeDestroy() {
|
||||
unsubscribe('nextcloud:unified-search:search', this.search)
|
||||
unsubscribe('nextcloud:unified-search:reset', this.resetSearch)
|
||||
unsubscribe('nextcloud:unified-search.search', this.search)
|
||||
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -280,12 +280,12 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
subscribe('nextcloud:unified-search:search', this.setSearch)
|
||||
subscribe('nextcloud:unified-search:reset', this.resetSearch)
|
||||
subscribe('nextcloud:unified-search.search', this.setSearch)
|
||||
subscribe('nextcloud:unified-search.reset', this.resetSearch)
|
||||
},
|
||||
beforeDestroy() {
|
||||
unsubscribe('nextcloud:unified-search:search', this.setSearch)
|
||||
unsubscribe('nextcloud:unified-search:reset', this.resetSearch)
|
||||
unsubscribe('nextcloud:unified-search.search', this.setSearch)
|
||||
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="section development-notice">
|
||||
<p>
|
||||
<a href="<?php p($_['reasons-use-nextcloud-pdf-link']); ?>" id="open-reasons-use-nextcloud-pdf" class="link-button icon-file" target="_blank">Reasons to use Nextcloud in your organization</a>
|
||||
<a href="<?php p($_['reasons-use-nextcloud-pdf-link']); ?>" id="open-reasons-use-nextcloud-pdf" class="link-button icon-file" target="_blank"><?php p($l->t('Reasons to use Nextcloud in your organization'));?></a>
|
||||
</p>
|
||||
<p>
|
||||
<?php print_unescaped(str_replace(
|
||||
|
||||
@@ -25,4 +25,7 @@
|
||||
<admin>OCA\Theming\Settings\Admin</admin>
|
||||
<admin-section>OCA\Theming\Settings\Section</admin-section>
|
||||
</settings>
|
||||
<commands>
|
||||
<command>OCA\Theming\Command\UpdateConfig</command>
|
||||
</commands>
|
||||
</info>
|
||||
|
||||
135
apps/theming/lib/Command/UpdateConfig.php
Normal file
135
apps/theming/lib/Command/UpdateConfig.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Theming\Command;
|
||||
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\IConfig;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class UpdateConfig extends Command {
|
||||
public const SUPPORTED_KEYS = [
|
||||
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color'
|
||||
];
|
||||
|
||||
public const SUPPORTED_IMAGE_KEYS = [
|
||||
'background', 'logo', 'favicon', 'logoheader'
|
||||
];
|
||||
|
||||
private $themingDefaults;
|
||||
private $imageManager;
|
||||
private $config;
|
||||
|
||||
public function __construct(ThemingDefaults $themingDefaults, ImageManager $imageManager, IConfig $config) {
|
||||
parent::__construct();
|
||||
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->imageManager = $imageManager;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
protected function configure() {
|
||||
$this
|
||||
->setName('theming:config')
|
||||
->setDescription('Set theming app config values')
|
||||
->addArgument(
|
||||
'key',
|
||||
InputArgument::OPTIONAL,
|
||||
'Key to update the theming app configuration (leave empty to get a list of all configured values)' . PHP_EOL .
|
||||
'One of: ' . implode(', ', self::SUPPORTED_KEYS)
|
||||
)
|
||||
->addArgument(
|
||||
'value',
|
||||
InputArgument::OPTIONAL,
|
||||
'Value to set (leave empty to obtain the current value)'
|
||||
)
|
||||
->addOption(
|
||||
'reset',
|
||||
'r',
|
||||
InputOption::VALUE_NONE,
|
||||
'Reset the given config key to default'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$key = $input->getArgument('key');
|
||||
$value = $input->getArgument('value');
|
||||
|
||||
if ($key === null) {
|
||||
$output->writeln('Current theming config:');
|
||||
foreach (self::SUPPORTED_KEYS as $key) {
|
||||
$value = $this->config->getAppValue('theming', $key, '');
|
||||
$output->writeln('- ' . $key . ': ' . $value . '');
|
||||
}
|
||||
foreach (self::SUPPORTED_IMAGE_KEYS as $key) {
|
||||
$value = $this->config->getAppValue('theming', $key . 'Mime', '');
|
||||
$output->writeln('- ' . $key . ': ' . $value . '');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!in_array($key, self::SUPPORTED_KEYS, true)) {
|
||||
$output->writeln('<error>Invalid config key provided</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($input->getOption('reset')) {
|
||||
$defaultValue = $this->themingDefaults->undo($key);
|
||||
$output->writeln('<info>Reset ' . $key . ' to ' . $defaultValue . '</info>');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$value = $this->config->getAppValue('theming', $key, '');
|
||||
if ($value !== '') {
|
||||
$output->writeln('<info>' . $key . ' is currently set to ' . $value . '</info>');
|
||||
} else {
|
||||
$output->writeln('<info>' . $key . ' is currently not set</info>');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (in_array($key, self::SUPPORTED_IMAGE_KEYS, true)) {
|
||||
if (file_exists(__DIR__ . $value)) {
|
||||
$value = __DIR__ . $value;
|
||||
}
|
||||
if (!file_exists($value)) {
|
||||
$output->writeln('<error>File could not be found: ' . $value . '</error>');
|
||||
return 1;
|
||||
}
|
||||
$value = $this->imageManager->updateImage($key, $value);
|
||||
$key = $key . 'Mime';
|
||||
}
|
||||
|
||||
$this->themingDefaults->set($key, $value);
|
||||
$output->writeln('<info>Updated ' . $key . ' to ' . $value . '</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -214,9 +214,6 @@ class ThemingController extends Controller {
|
||||
* @throws NotPermittedException
|
||||
*/
|
||||
public function uploadImage(): DataResponse {
|
||||
// logo / background
|
||||
// new: favicon logo-header
|
||||
//
|
||||
$key = $this->request->getParam('key');
|
||||
$image = $this->request->getUploadedFile('image');
|
||||
$error = null;
|
||||
@@ -249,23 +246,14 @@ class ThemingController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
$name = '';
|
||||
try {
|
||||
$folder = $this->appData->getFolder('images');
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $this->appData->newFolder('images');
|
||||
}
|
||||
|
||||
$this->imageManager->delete($key);
|
||||
|
||||
$target = $folder->newFile($key);
|
||||
$supportedFormats = $this->getSupportedUploadImageFormats($key);
|
||||
$detectedMimeType = mime_content_type($image['tmp_name']);
|
||||
if (!in_array($image['type'], $supportedFormats) || !in_array($detectedMimeType, $supportedFormats)) {
|
||||
$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
|
||||
$this->themingDefaults->set($key . 'Mime', $mime);
|
||||
} catch (\Exception $e) {
|
||||
return new DataResponse(
|
||||
[
|
||||
'data' => [
|
||||
'message' => $this->l10n->t('Unsupported image type'),
|
||||
'message' => $e->getMessage()
|
||||
],
|
||||
'status' => 'failure',
|
||||
],
|
||||
@@ -273,28 +261,7 @@ class ThemingController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false) {
|
||||
// Optimize the image since some people may upload images that will be
|
||||
// either to big or are not progressive rendering.
|
||||
$newImage = @imagecreatefromstring(file_get_contents($image['tmp_name'], 'r'));
|
||||
|
||||
$tmpFile = $this->tempManager->getTemporaryFile();
|
||||
$newWidth = imagesx($newImage) < 4096 ? imagesx($newImage) : 4096;
|
||||
$newHeight = imagesy($newImage) / (imagesx($newImage) / $newWidth);
|
||||
$outputImage = imagescale($newImage, $newWidth, $newHeight);
|
||||
|
||||
imageinterlace($outputImage, 1);
|
||||
imagejpeg($outputImage, $tmpFile, 75);
|
||||
imagedestroy($outputImage);
|
||||
|
||||
$target->putContent(file_get_contents($tmpFile, 'r'));
|
||||
} else {
|
||||
$target->putContent(file_get_contents($image['tmp_name'], 'r'));
|
||||
}
|
||||
$name = $image['name'];
|
||||
|
||||
$this->themingDefaults->set($key.'Mime', $image['type']);
|
||||
|
||||
$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
|
||||
|
||||
return new DataResponse(
|
||||
@@ -311,24 +278,6 @@ class ThemingController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of supported mime types for image uploads.
|
||||
* "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
|
||||
*
|
||||
* @param string $key The image key, e.g. "favicon"
|
||||
* @return array
|
||||
*/
|
||||
private function getSupportedUploadImageFormats(string $key): array {
|
||||
$supportedFormats = ['image/jpeg', 'image/png', 'image/gif',];
|
||||
|
||||
if ($key !== 'favicon' || $this->imageManager->shouldReplaceIcons() === true) {
|
||||
$supportedFormats[] = 'image/svg+xml';
|
||||
$supportedFormats[] = 'image/svg';
|
||||
}
|
||||
|
||||
return $supportedFormats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert setting to default value
|
||||
*
|
||||
@@ -341,11 +290,6 @@ class ThemingController extends Controller {
|
||||
// reprocess server scss for preview
|
||||
$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
|
||||
|
||||
if (strpos($setting, 'Mime') !== -1) {
|
||||
$imageKey = str_replace('Mime', '', $setting);
|
||||
$this->imageManager->delete($imageKey);
|
||||
}
|
||||
|
||||
return new DataResponse(
|
||||
[
|
||||
'data' =>
|
||||
|
||||
@@ -36,6 +36,7 @@ use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
use OCP\ILogger;
|
||||
use OCP\ITempManager;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class ImageManager {
|
||||
@@ -52,27 +53,22 @@ class ImageManager {
|
||||
private $cacheFactory;
|
||||
/** @var ILogger */
|
||||
private $logger;
|
||||
/** @var ITempManager */
|
||||
private $tempManager;
|
||||
|
||||
/**
|
||||
* ImageManager constructor.
|
||||
*
|
||||
* @param IConfig $config
|
||||
* @param IAppData $appData
|
||||
* @param IURLGenerator $urlGenerator
|
||||
* @param ICacheFactory $cacheFactory
|
||||
* @param ILogger $logger
|
||||
*/
|
||||
public function __construct(IConfig $config,
|
||||
IAppData $appData,
|
||||
IURLGenerator $urlGenerator,
|
||||
ICacheFactory $cacheFactory,
|
||||
ILogger $logger
|
||||
ILogger $logger,
|
||||
ITempManager $tempManager
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->appData = $appData;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cacheFactory = $cacheFactory;
|
||||
$this->logger = $logger;
|
||||
$this->tempManager = $tempManager;
|
||||
}
|
||||
|
||||
public function getImageUrl(string $key, bool $useSvg = true): string {
|
||||
@@ -211,6 +207,62 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
public function updateImage(string $key, string $tmpFile) {
|
||||
$this->delete($key);
|
||||
|
||||
try {
|
||||
$folder = $this->appData->getFolder('images');
|
||||
} catch (NotFoundException $e) {
|
||||
$folder = $this->appData->newFolder('images');
|
||||
}
|
||||
|
||||
$target = $folder->newFile($key);
|
||||
$supportedFormats = $this->getSupportedUploadImageFormats($key);
|
||||
$detectedMimeType = mime_content_type($tmpFile);
|
||||
if (!in_array($detectedMimeType, $supportedFormats, true)) {
|
||||
throw new \Exception('Unsupported image type');
|
||||
}
|
||||
|
||||
if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false) {
|
||||
// Optimize the image since some people may upload images that will be
|
||||
// either to big or are not progressive rendering.
|
||||
$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
|
||||
|
||||
$tmpFile = $this->tempManager->getTemporaryFile();
|
||||
$newWidth = (int)(imagesx($newImage) < 4096 ? imagesx($newImage) : 4096);
|
||||
$newHeight = (int)(imagesy($newImage) / (imagesx($newImage) / $newWidth));
|
||||
$outputImage = imagescale($newImage, $newWidth, $newHeight);
|
||||
|
||||
imageinterlace($outputImage, 1);
|
||||
imagejpeg($outputImage, $tmpFile, 75);
|
||||
imagedestroy($outputImage);
|
||||
|
||||
$target->putContent(file_get_contents($tmpFile));
|
||||
} else {
|
||||
$target->putContent(file_get_contents($tmpFile));
|
||||
}
|
||||
|
||||
return $detectedMimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of supported mime types for image uploads.
|
||||
* "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
|
||||
*
|
||||
* @param string $key The image key, e.g. "favicon"
|
||||
* @return array
|
||||
*/
|
||||
private function getSupportedUploadImageFormats(string $key): array {
|
||||
$supportedFormats = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
|
||||
if ($key !== 'favicon' || $this->shouldReplaceIcons() === true) {
|
||||
$supportedFormats[] = 'image/svg+xml';
|
||||
$supportedFormats[] = 'image/svg';
|
||||
}
|
||||
|
||||
return $supportedFormats;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove cached files that are not required any longer
|
||||
*
|
||||
|
||||
@@ -398,6 +398,7 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
$this->config->deleteAppValue('theming', $setting);
|
||||
$this->increaseCacheBuster();
|
||||
|
||||
$returnValue = '';
|
||||
switch ($setting) {
|
||||
case 'name':
|
||||
$returnValue = $this->getEntity();
|
||||
@@ -411,8 +412,11 @@ class ThemingDefaults extends \OC_Defaults {
|
||||
case 'color':
|
||||
$returnValue = $this->getColorPrimary();
|
||||
break;
|
||||
default:
|
||||
$returnValue = '';
|
||||
case 'logo':
|
||||
case 'logoheader':
|
||||
case 'background':
|
||||
case 'favicon':
|
||||
$this->imageManager->delete($setting);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,34 +47,32 @@ use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IRequest;
|
||||
use OCP\ITempManager;
|
||||
use OCP\IURLGenerator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class ThemingControllerTest extends TestCase {
|
||||
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IRequest|MockObject */
|
||||
private $request;
|
||||
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IConfig|MockObject */
|
||||
private $config;
|
||||
/** @var ThemingDefaults|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var ThemingDefaults|MockObject */
|
||||
private $themingDefaults;
|
||||
/** @var \OCP\AppFramework\Utility\ITimeFactory */
|
||||
private $timeFactory;
|
||||
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IL10N|MockObject */
|
||||
private $l10n;
|
||||
/** @var ThemingController */
|
||||
private $themingController;
|
||||
/** @var ITempManager */
|
||||
private $tempManager;
|
||||
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IAppManager|MockObject */
|
||||
private $appManager;
|
||||
/** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IAppData|MockObject */
|
||||
private $appData;
|
||||
/** @var ImageManager|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var ImageManager|MockObject */
|
||||
private $imageManager;
|
||||
/** @var SCSSCacher */
|
||||
private $scssCacher;
|
||||
@@ -93,12 +91,12 @@ class ThemingControllerTest extends TestCase {
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->imageManager = $this->createMock(ImageManager::class);
|
||||
|
||||
$this->timeFactory = $this->createMock(ITimeFactory::class);
|
||||
$this->timeFactory->expects($this->any())
|
||||
$timeFactory = $this->createMock(ITimeFactory::class);
|
||||
$timeFactory->expects($this->any())
|
||||
->method('getTime')
|
||||
->willReturn(123);
|
||||
|
||||
$this->overwriteService(ITimeFactory::class, $this->timeFactory);
|
||||
$this->overwriteService(ITimeFactory::class, $timeFactory);
|
||||
|
||||
$this->themingController = new ThemingController(
|
||||
'theming',
|
||||
@@ -287,12 +285,9 @@ class ThemingControllerTest extends TestCase {
|
||||
return $str;
|
||||
});
|
||||
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
$this->appData
|
||||
->expects($this->once())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('updateImage')
|
||||
->willThrowException(new \Exception('Unsupported image type'));
|
||||
|
||||
$expected = new DataResponse(
|
||||
[
|
||||
@@ -331,12 +326,9 @@ class ThemingControllerTest extends TestCase {
|
||||
return $str;
|
||||
});
|
||||
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
$this->appData
|
||||
->expects($this->once())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('updateImage')
|
||||
->willThrowException(new \Exception('Unsupported image type'));
|
||||
|
||||
$expected = new DataResponse(
|
||||
[
|
||||
@@ -392,31 +384,6 @@ class ThemingControllerTest extends TestCase {
|
||||
return $str;
|
||||
});
|
||||
|
||||
|
||||
$file = $this->createMock(ISimpleFile::class);
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
if ($folderExists) {
|
||||
$this->appData
|
||||
->expects($this->once())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
} else {
|
||||
$this->appData
|
||||
->expects($this->at(0))
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willThrowException(new NotFoundException());
|
||||
$this->appData
|
||||
->expects($this->at(1))
|
||||
->method('newFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
}
|
||||
$folder->expects($this->once())
|
||||
->method('newFile')
|
||||
->with('logo')
|
||||
->willReturn($file);
|
||||
$this->urlGenerator->expects($this->once())
|
||||
->method('linkTo')
|
||||
->willReturn('serverCss');
|
||||
@@ -424,6 +391,10 @@ class ThemingControllerTest extends TestCase {
|
||||
->method('getImageUrl')
|
||||
->with('logo')
|
||||
->willReturn('imageUrl');
|
||||
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('updateImage');
|
||||
|
||||
$expected = new DataResponse(
|
||||
[
|
||||
'data' =>
|
||||
@@ -468,30 +439,8 @@ class ThemingControllerTest extends TestCase {
|
||||
return $str;
|
||||
});
|
||||
|
||||
$file = $this->createMock(ISimpleFile::class);
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
if ($folderExists) {
|
||||
$this->appData
|
||||
->expects($this->once())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
} else {
|
||||
$this->appData
|
||||
->expects($this->at(0))
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willThrowException(new NotFoundException());
|
||||
$this->appData
|
||||
->expects($this->at(1))
|
||||
->method('newFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
}
|
||||
$folder->expects($this->once())
|
||||
->method('newFile')
|
||||
->with('background')
|
||||
->willReturn($file);
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('updateImage');
|
||||
|
||||
$this->urlGenerator->expects($this->once())
|
||||
->method('linkTo')
|
||||
@@ -542,12 +491,9 @@ class ThemingControllerTest extends TestCase {
|
||||
return $str;
|
||||
});
|
||||
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
$this->appData
|
||||
->expects($this->once())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('updateImage')
|
||||
->willThrowException(new \Exception('Unsupported image type'));
|
||||
|
||||
$expected = new DataResponse(
|
||||
[
|
||||
@@ -717,9 +663,6 @@ class ThemingControllerTest extends TestCase {
|
||||
->method('linkTo')
|
||||
->with('', '/core/css/someHash-css-variables.scss')
|
||||
->willReturn('/nextcloudWebroot/core/css/someHash-css-variables.scss');
|
||||
$this->imageManager->expects($this->once())
|
||||
->method('delete')
|
||||
->with($filename);
|
||||
|
||||
$expected = new DataResponse(
|
||||
[
|
||||
|
||||
@@ -36,23 +36,27 @@ use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
use OCP\ILogger;
|
||||
use OCP\ITempManager;
|
||||
use OCP\IURLGenerator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class ImageManagerTest extends TestCase {
|
||||
|
||||
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IConfig|MockObject */
|
||||
protected $config;
|
||||
/** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IAppData|MockObject */
|
||||
protected $appData;
|
||||
/** @var ImageManager */
|
||||
protected $imageManager;
|
||||
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IURLGenerator|MockObject */
|
||||
private $urlGenerator;
|
||||
/** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var ICacheFactory|MockObject */
|
||||
private $cacheFactory;
|
||||
/** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var ILogger|MockObject */
|
||||
private $logger;
|
||||
/** @var ITempManager|MockObject */
|
||||
private $tempManager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
@@ -61,12 +65,14 @@ class ImageManagerTest extends TestCase {
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->cacheFactory = $this->createMock(ICacheFactory::class);
|
||||
$this->logger = $this->createMock(ILogger::class);
|
||||
$this->tempManager = $this->createMock(ITempManager::class);
|
||||
$this->imageManager = new ImageManager(
|
||||
$this->config,
|
||||
$this->appData,
|
||||
$this->urlGenerator,
|
||||
$this->cacheFactory,
|
||||
$this->logger
|
||||
$this->logger,
|
||||
$this->tempManager
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,7 +90,7 @@ class ImageManagerTest extends TestCase {
|
||||
}
|
||||
|
||||
public function mockGetImage($key, $file) {
|
||||
/** @var \PHPUnit\Framework\MockObject\MockObject $folder */
|
||||
/** @var MockObject $folder */
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
if ($file === null) {
|
||||
$folder->expects($this->once())
|
||||
@@ -327,4 +333,56 @@ class ImageManagerTest extends TestCase {
|
||||
->willReturn($folders[2]);
|
||||
$this->imageManager->cleanup();
|
||||
}
|
||||
|
||||
|
||||
public function dataUpdateImage() {
|
||||
return [
|
||||
['background', __DIR__ . '/../../../tests/data/testimage.png', true, true],
|
||||
['background', __DIR__ . '/../../../tests/data/testimage.png', false, true],
|
||||
['background', __DIR__ . '/../../../tests/data/testimage.jpg', true, true],
|
||||
['logo', __DIR__ . '/../../../tests/data/testimagelarge.svg', true, false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataUpdateImage
|
||||
*/
|
||||
public function testUpdateImage($key, $tmpFile, $folderExists, $shouldConvert) {
|
||||
$file = $this->createMock(ISimpleFile::class);
|
||||
$folder = $this->createMock(ISimpleFolder::class);
|
||||
$oldFile = $this->createMock(ISimpleFile::class);
|
||||
$folder->expects($this->any())
|
||||
->method('getFile')
|
||||
->willReturn($oldFile);
|
||||
if ($folderExists) {
|
||||
$this->appData
|
||||
->expects($this->any())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
} else {
|
||||
$this->appData
|
||||
->expects($this->any())
|
||||
->method('getFolder')
|
||||
->with('images')
|
||||
->willThrowException(new NotFoundException());
|
||||
$this->appData
|
||||
->expects($this->any())
|
||||
->method('newFolder')
|
||||
->with('images')
|
||||
->willReturn($folder);
|
||||
}
|
||||
$folder->expects($this->once())
|
||||
->method('newFile')
|
||||
->with($key)
|
||||
->willReturn($file);
|
||||
|
||||
if ($shouldConvert) {
|
||||
$this->tempManager->expects($this->once())
|
||||
->method('getTemporaryFile')
|
||||
->willReturn('/tmp/randomtempfile-theming');
|
||||
}
|
||||
|
||||
$this->imageManager->updateImage($key, $tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
apps/user_status/js/user-status-modal.js
Normal file
2
apps/user_status/js/user-status-modal.js
Normal file
File diff suppressed because one or more lines are too long
1
apps/user_status/js/user-status-modal.js.map
Normal file
1
apps/user_status/js/user-status-modal.js.map
Normal file
File diff suppressed because one or more lines are too long
2
apps/user_status/js/vendors-user-status-modal.js
Normal file
2
apps/user_status/js/vendors-user-status-modal.js
Normal file
File diff suppressed because one or more lines are too long
1
apps/user_status/js/vendors-user-status-modal.js.map
Normal file
1
apps/user_status/js/vendors-user-status-modal.js.map
Normal file
File diff suppressed because one or more lines are too long
0
apps/user_status/l10n/.gitkeep
Normal file
0
apps/user_status/l10n/.gitkeep
Normal file
@@ -138,7 +138,12 @@ class UserStatusController extends OCSController {
|
||||
string $message,
|
||||
?int $clearAt): DataResponse {
|
||||
try {
|
||||
$status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt);
|
||||
if ($message !== '') {
|
||||
$status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt);
|
||||
} else {
|
||||
$this->service->clearMessage($this->userId);
|
||||
$status = $this->service->findByUserId($this->userId);
|
||||
}
|
||||
return new DataResponse($this->formatStatus($status));
|
||||
} catch (InvalidClearAtException $ex) {
|
||||
$this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid clearAt value "' . $clearAt . '"');
|
||||
|
||||
@@ -77,7 +77,7 @@ class StatusService {
|
||||
];
|
||||
|
||||
/** @var int */
|
||||
public const INVALIDATE_STATUS_THRESHOLD = 5 /* minutes */ * 60 /* seconds */;
|
||||
public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */;
|
||||
|
||||
/** @var int */
|
||||
public const MAXIMUM_MESSAGE_LENGTH = 80;
|
||||
|
||||
@@ -20,70 +20,59 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<li :class="{ inline }">
|
||||
<div id="user-status-menu-item">
|
||||
<li>
|
||||
<div class="user-status-menu-item">
|
||||
<!-- Username display -->
|
||||
<span
|
||||
v-if="!inline"
|
||||
id="user-status-menu-item__header"
|
||||
class="user-status-menu-item__header"
|
||||
:title="displayName">
|
||||
{{ displayName }}
|
||||
</span>
|
||||
<Actions
|
||||
id="user-status-menu-item__subheader"
|
||||
:default-icon="statusIcon"
|
||||
container="header"
|
||||
:menu-title="visibleMessage"
|
||||
:title="visibleMessage">
|
||||
<ActionButton
|
||||
v-for="status in statuses"
|
||||
:key="status.type"
|
||||
:icon="status.icon"
|
||||
:close-after-click="true"
|
||||
:title="status.label"
|
||||
@click.prevent.stop="changeStatus(status.type)">
|
||||
{{ status.subline }}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
icon="icon-rename"
|
||||
:close-after-click="true"
|
||||
:title="$t('user_status', 'Set custom status')"
|
||||
@click.prevent.stop="openModal" />
|
||||
</Actions>
|
||||
<SetStatusModal
|
||||
v-if="isModalOpen"
|
||||
@close="closeModal" />
|
||||
|
||||
<!-- Status modal toggle -->
|
||||
<toggle :is="inline ? 'button' : 'a'"
|
||||
:class="{'user-status-menu-item__toggle--inline': inline}"
|
||||
class="user-status-menu-item__toggle"
|
||||
href="#"
|
||||
@click.prevent.stop="openModal">
|
||||
<span :class="statusIcon" class="user-status-menu-item__toggle-icon" />
|
||||
{{ visibleMessage }}
|
||||
</toggle>
|
||||
</div>
|
||||
|
||||
<!-- Status management modal -->
|
||||
<SetStatusModal
|
||||
v-if="isModalOpen"
|
||||
@close="closeModal" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import SetStatusModal from './components/SetStatusModal'
|
||||
import Actions from '@nextcloud/vue/dist/Components/Actions'
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||
import { mapState } from 'vuex'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { getAllStatusOptions } from './services/statusOptionsService'
|
||||
import { sendHeartbeat } from './services/heartbeatService'
|
||||
import debounce from 'debounce'
|
||||
|
||||
import { sendHeartbeat } from './services/heartbeatService'
|
||||
import OnlineStatusMixin from './mixins/OnlineStatusMixin'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
name: 'UserStatus',
|
||||
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
SetStatusModal,
|
||||
SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal'),
|
||||
},
|
||||
mixins: [OnlineStatusMixin],
|
||||
|
||||
props: {
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
statuses: getAllStatusOptions(),
|
||||
heartbeatInterval: null,
|
||||
setAwayTimeout: null,
|
||||
mouseMoveListener: null,
|
||||
@@ -91,12 +80,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
statusType: state => state.userStatus.status,
|
||||
statusIsUserDefined: state => state.userStatus.statusIsUserDefined,
|
||||
customIcon: state => state.userStatus.icon,
|
||||
customMessage: state => state.userStatus.message,
|
||||
}),
|
||||
/**
|
||||
* The display-name of the current user
|
||||
*
|
||||
@@ -105,64 +88,8 @@ export default {
|
||||
displayName() {
|
||||
return getCurrentUser().displayName
|
||||
},
|
||||
/**
|
||||
* The message displayed in the top right corner
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
visibleMessage() {
|
||||
if (this.customIcon && this.customMessage) {
|
||||
return `${this.customIcon} ${this.customMessage}`
|
||||
}
|
||||
if (this.customMessage) {
|
||||
return this.customMessage
|
||||
}
|
||||
|
||||
if (this.statusIsUserDefined) {
|
||||
switch (this.statusType) {
|
||||
case 'online':
|
||||
return this.$t('user_status', 'Online')
|
||||
|
||||
case 'away':
|
||||
return this.$t('user_status', 'Away')
|
||||
|
||||
case 'dnd':
|
||||
return this.$t('user_status', 'Do not disturb')
|
||||
|
||||
case 'invisible':
|
||||
return this.$t('user_status', 'Invisible')
|
||||
|
||||
case 'offline':
|
||||
return this.$t('user_status', 'Offline')
|
||||
}
|
||||
}
|
||||
|
||||
return this.$t('user_status', 'Set status')
|
||||
},
|
||||
/**
|
||||
* The status indicator icon
|
||||
*
|
||||
* @returns {String|null}
|
||||
*/
|
||||
statusIcon() {
|
||||
switch (this.statusType) {
|
||||
case 'online':
|
||||
return 'icon-user-status-online'
|
||||
|
||||
case 'away':
|
||||
return 'icon-user-status-away'
|
||||
|
||||
case 'dnd':
|
||||
return 'icon-user-status-dnd'
|
||||
|
||||
case 'invisible':
|
||||
case 'offline':
|
||||
return 'icon-user-status-invisible'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the current user's status from initial state
|
||||
* and stores it in Vuex
|
||||
@@ -198,6 +125,7 @@ export default {
|
||||
this._backgroundHeartbeat()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Some housekeeping before destroying the component
|
||||
*/
|
||||
@@ -205,6 +133,7 @@ export default {
|
||||
window.removeEventListener('mouseMove', this.mouseMoveListener)
|
||||
clearInterval(this.heartbeatInterval)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Opens the modal to set a custom status
|
||||
@@ -218,19 +147,7 @@ export default {
|
||||
closeModal() {
|
||||
this.isModalOpen = false
|
||||
},
|
||||
/**
|
||||
* Changes the user-status
|
||||
*
|
||||
* @param {String} statusType (online / away / dnd / invisible)
|
||||
*/
|
||||
async changeStatus(statusType) {
|
||||
try {
|
||||
await this.$store.dispatch('setStatus', { statusType })
|
||||
} catch (err) {
|
||||
showError(this.$t('user_status', 'There was an error saving the new status'))
|
||||
console.debug(err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends the status heartbeat to the server
|
||||
*
|
||||
@@ -248,65 +165,55 @@ export default {
|
||||
<style lang="scss">
|
||||
$max-width-user-status: 200px;
|
||||
|
||||
li:not(.inline) #user-status-menu-item {
|
||||
.user-status-menu-item {
|
||||
&__header {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-text-maxcontrast);
|
||||
padding: 10px 12px 5px 38px;
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
max-width: $max-width-user-status;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
max-width: $max-width-user-status;
|
||||
padding: 10px 12px 5px 38px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 1;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
&__subheader {
|
||||
width: 100%;
|
||||
|
||||
button.action-item__menutoggle {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-main-background);
|
||||
background-position: 12px center;
|
||||
&__toggle {
|
||||
&-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 10px;
|
||||
opacity: 1 !important;
|
||||
background-size: 16px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
font-weight: normal;
|
||||
padding-left: 38px;
|
||||
opacity: 1;
|
||||
max-width: $max-width-user-status;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// In dashboard
|
||||
&--inline {
|
||||
width: auto;
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-pill);
|
||||
background-color: var(--color-background-translucent);
|
||||
font-size: inherit;
|
||||
font-weight: normal;
|
||||
|
||||
-webkit-backdrop-filter: var(--background-blur);
|
||||
backdrop-filter: var(--background-blur);
|
||||
|
||||
&:active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: inset 4px 0 var(--color-primary-element);
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline #user-status-menu-item__subheader {
|
||||
width: 100%;
|
||||
|
||||
button.action-item__menutoggle {
|
||||
background-size: 16px;
|
||||
border: 0;
|
||||
border-radius: var(--border-radius-pill);
|
||||
font-weight: normal;
|
||||
padding-left: 40px;
|
||||
|
||||
&.icon-loading-small {
|
||||
&::after {
|
||||
left: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
||||
122
apps/user_status/src/components/OnlineStatusSelect.vue
Normal file
122
apps/user_status/src/components/OnlineStatusSelect.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<!--
|
||||
- @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<div class="user-status-online-select">
|
||||
<input :id="id"
|
||||
:checked="checked"
|
||||
class="user-status-online-select__input"
|
||||
type="radio"
|
||||
name="user-status-online"
|
||||
@change="onChange">
|
||||
<label :for="id" :class="icon" class="user-status-online-select__label">
|
||||
{{ label }}
|
||||
<em class="user-status-online-select__subline">{{ subline }}</em>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OnlineStatusSelect',
|
||||
|
||||
props: {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subline: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
id() {
|
||||
return `user-status-online-status-${this.type}`
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChange() {
|
||||
this.$emit('select', this.type)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$icon-size: 24px;
|
||||
$label-padding: 8px;
|
||||
|
||||
.user-status-online-select {
|
||||
// Inputs are here for keyboard navigation, they are not visually visible
|
||||
&__input {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
left: -10000px;
|
||||
overflow: hidden;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
margin: $label-padding;
|
||||
padding: $label-padding;
|
||||
padding-left: $icon-size + $label-padding * 2;
|
||||
border: 2px solid var(--color-main-background);
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: var(--color-background-hover);
|
||||
background-position: $label-padding center;
|
||||
background-size: $icon-size;
|
||||
|
||||
span,
|
||||
& {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__input:checked + &__label,
|
||||
&__input:focus + &__label,
|
||||
&__label:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&__subline {
|
||||
display: block;
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -22,11 +22,24 @@
|
||||
<template>
|
||||
<Modal
|
||||
size="normal"
|
||||
:title="$t('user_status', 'Set a custom status')"
|
||||
:title="$t('user_status', 'Set status')"
|
||||
@close="closeModal">
|
||||
<div class="set-status-modal">
|
||||
<!-- Status selector -->
|
||||
<div class="set-status-modal__header">
|
||||
<h3>{{ $t('user_status', 'Set a custom status') }}</h3>
|
||||
<h3>{{ $t('user_status', 'Online status') }}</h3>
|
||||
</div>
|
||||
<div class="set-status-modal__online-status">
|
||||
<OnlineStatusSelect v-for="status in statuses"
|
||||
:key="status.type"
|
||||
v-bind="status"
|
||||
:checked="status.type === statusType"
|
||||
@select="changeStatus" />
|
||||
</div>
|
||||
|
||||
<!-- Status message -->
|
||||
<div class="set-status-modal__header">
|
||||
<h3>{{ $t('user_status', 'Status message') }}</h3>
|
||||
</div>
|
||||
<div class="set-status-modal__custom-input">
|
||||
<EmojiPicker @select="setIcon">
|
||||
@@ -46,10 +59,10 @@
|
||||
@selectClearAt="setClearAt" />
|
||||
<div class="status-buttons">
|
||||
<button class="status-buttons__select" @click="clearStatus">
|
||||
{{ $t('user_status', 'Clear custom status') }}
|
||||
{{ $t('user_status', 'Clear status message') }}
|
||||
</button>
|
||||
<button class="status-buttons__primary primary" @click="saveStatus">
|
||||
{{ $t('user_status', 'Set status') }}
|
||||
{{ $t('user_status', 'Set status message') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,27 +70,36 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
|
||||
import { getAllStatusOptions } from '../services/statusOptionsService'
|
||||
import OnlineStatusMixin from '../mixins/OnlineStatusMixin'
|
||||
import PredefinedStatusesList from './PredefinedStatusesList'
|
||||
import CustomMessageInput from './CustomMessageInput'
|
||||
import ClearAtSelect from './ClearAtSelect'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import OnlineStatusSelect from './OnlineStatusSelect'
|
||||
|
||||
export default {
|
||||
name: 'SetStatusModal',
|
||||
|
||||
components: {
|
||||
ClearAtSelect,
|
||||
CustomMessageInput,
|
||||
EmojiPicker,
|
||||
Modal,
|
||||
CustomMessageInput,
|
||||
OnlineStatusSelect,
|
||||
PredefinedStatusesList,
|
||||
ClearAtSelect,
|
||||
},
|
||||
mixins: [OnlineStatusMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
clearAt: null,
|
||||
icon: null,
|
||||
message: null,
|
||||
clearAt: null,
|
||||
statuses: getAllStatusOptions(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -90,6 +112,7 @@ export default {
|
||||
return this.icon || '😀'
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the current status when a user opens dialog
|
||||
*/
|
||||
@@ -208,6 +231,21 @@ export default {
|
||||
min-width: 500px;
|
||||
min-height: 200px;
|
||||
padding: 8px 20px 20px 20px;
|
||||
// Enable scrollbar for too long content, same way as in Dashboard customize
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__online-status {
|
||||
display: grid;
|
||||
// Space between the two sections
|
||||
margin-bottom: 40px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
&__custom-input {
|
||||
display: flex;
|
||||
@@ -216,12 +254,12 @@ export default {
|
||||
|
||||
.custom-input__emoji-button {
|
||||
flex-basis: 40px;
|
||||
width: 40px;
|
||||
flex-grow: 0;
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
width: 40px;
|
||||
height: 34px;
|
||||
margin-right: 0;
|
||||
border-right: none;
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,4 +271,5 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
import App from './App'
|
||||
import UserStatus from './UserStatus'
|
||||
import store from './store'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
@@ -36,18 +36,23 @@ __webpack_public_path__ = OC.linkTo('user_status', 'js/')
|
||||
Vue.prototype.t = t
|
||||
Vue.prototype.$t = t
|
||||
|
||||
const app = new Vue({
|
||||
render: h => h(App),
|
||||
// Register settings menu entry
|
||||
export default new Vue({
|
||||
el: 'li[data-id="user_status-menuitem"]',
|
||||
// eslint-disable-next-line vue/match-component-file-name
|
||||
name: 'UserStatusRoot',
|
||||
render: h => h(UserStatus),
|
||||
store,
|
||||
}).$mount('li[data-id="user_status-menuitem"]')
|
||||
})
|
||||
|
||||
// Register dashboard status
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!OCA.Dashboard) {
|
||||
return
|
||||
}
|
||||
|
||||
OCA.Dashboard.registerStatus('status', (el) => {
|
||||
const Dashboard = Vue.extend(App)
|
||||
const Dashboard = Vue.extend(UserStatus)
|
||||
return new Dashboard({
|
||||
propsData: {
|
||||
inline: true,
|
||||
@@ -56,5 +61,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}).$mount(el)
|
||||
})
|
||||
})
|
||||
|
||||
export { app }
|
||||
|
||||
110
apps/user_status/src/mixins/OnlineStatusMixin.js
Normal file
110
apps/user_status/src/mixins/OnlineStatusMixin.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { mapState } from 'vuex'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
statusType: state => state.userStatus.status,
|
||||
statusIsUserDefined: state => state.userStatus.statusIsUserDefined,
|
||||
customIcon: state => state.userStatus.icon,
|
||||
customMessage: state => state.userStatus.message,
|
||||
}),
|
||||
|
||||
/**
|
||||
* The message displayed in the top right corner
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
visibleMessage() {
|
||||
if (this.customIcon && this.customMessage) {
|
||||
return `${this.customIcon} ${this.customMessage}`
|
||||
}
|
||||
|
||||
if (this.customMessage) {
|
||||
return this.customMessage
|
||||
}
|
||||
|
||||
if (this.statusIsUserDefined) {
|
||||
switch (this.statusType) {
|
||||
case 'online':
|
||||
return this.$t('user_status', 'Online')
|
||||
|
||||
case 'away':
|
||||
return this.$t('user_status', 'Away')
|
||||
|
||||
case 'dnd':
|
||||
return this.$t('user_status', 'Do not disturb')
|
||||
|
||||
case 'invisible':
|
||||
return this.$t('user_status', 'Invisible')
|
||||
|
||||
case 'offline':
|
||||
return this.$t('user_status', 'Offline')
|
||||
}
|
||||
}
|
||||
|
||||
return this.$t('user_status', 'Set status')
|
||||
},
|
||||
|
||||
/**
|
||||
* The status indicator icon
|
||||
*
|
||||
* @returns {String|null}
|
||||
*/
|
||||
statusIcon() {
|
||||
switch (this.statusType) {
|
||||
case 'online':
|
||||
return 'icon-user-status-online'
|
||||
|
||||
case 'away':
|
||||
return 'icon-user-status-away'
|
||||
|
||||
case 'dnd':
|
||||
return 'icon-user-status-dnd'
|
||||
|
||||
case 'invisible':
|
||||
case 'offline':
|
||||
return 'icon-user-status-invisible'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Changes the user-status
|
||||
*
|
||||
* @param {String} statusType (online / away / dnd / invisible)
|
||||
*/
|
||||
async changeStatus(statusType) {
|
||||
try {
|
||||
await this.$store.dispatch('setStatus', { statusType })
|
||||
} catch (err) {
|
||||
showError(this.$t('user_status', 'There was an error saving the new status'))
|
||||
console.debug(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -44,6 +44,7 @@ const getAllStatusOptions = () => {
|
||||
}, {
|
||||
type: 'invisible',
|
||||
label: t('user_status', 'Invisible'),
|
||||
subline: t('user_status', 'Appear offline'),
|
||||
icon: 'icon-user-status-invisible',
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@
|
||||
id="user-status_panel"
|
||||
:items="items"
|
||||
:loading="loading">
|
||||
<template v-slot:default="{ item }">
|
||||
<DashboardWidgetItem
|
||||
:main-text="item.mainText"
|
||||
:sub-text="item.subText">
|
||||
<template v-slot:avatar>
|
||||
<Avatar
|
||||
class="item-avatar"
|
||||
:size="44"
|
||||
:user="item.avatarUsername"
|
||||
:display-name="item.mainText"
|
||||
:show-user-status-compact="false" />
|
||||
</template>
|
||||
</DashboardWidgetItem>
|
||||
</template>
|
||||
<template v-slot:empty-content>
|
||||
<EmptyContent
|
||||
id="user_status-widget-empty-content"
|
||||
@@ -35,7 +49,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DashboardWidget } from '@nextcloud/vue-dashboard'
|
||||
import { DashboardWidget, DashboardWidgetItem } from '@nextcloud/vue-dashboard'
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import moment from '@nextcloud/moment'
|
||||
@@ -43,7 +58,9 @@ import moment from '@nextcloud/moment'
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
Avatar,
|
||||
DashboardWidget,
|
||||
DashboardWidgetItem,
|
||||
EmptyContent,
|
||||
},
|
||||
data() {
|
||||
@@ -56,13 +73,21 @@ export default {
|
||||
items() {
|
||||
return this.statuses.map((item) => {
|
||||
const icon = item.icon || ''
|
||||
const message = item.message || ''
|
||||
const status = `${icon} ${message}`
|
||||
let message = item.message || ''
|
||||
if (message === '') {
|
||||
if (item.status === 'away') {
|
||||
message = t('user_status', 'Away')
|
||||
}
|
||||
if (item.status === 'dnd') {
|
||||
message = t('user_status', 'Do not disturb')
|
||||
}
|
||||
}
|
||||
const status = item.icon !== '' ? `${icon} ${message}` : message
|
||||
|
||||
let subText
|
||||
if (item.icon === null && item.message === null && item.timestamp === null) {
|
||||
if (item.icon === null && message === '' && item.timestamp === null) {
|
||||
subText = ''
|
||||
} else if (item.icon === null && item.message === null && item.timestamp !== null) {
|
||||
} else if (item.icon === null && message === '' && item.timestamp !== null) {
|
||||
subText = moment(item.timestamp, 'X').fromNow()
|
||||
} else if (item.timestamp !== null) {
|
||||
subText = this.t('user_status', '{status}, {timestamp}', {
|
||||
|
||||
@@ -56,7 +56,7 @@ class ClearOldStatusesBackgroundJobTest extends TestCase {
|
||||
->with(1337);
|
||||
$this->mapper->expects($this->once())
|
||||
->method('clearStatusesOlderThan')
|
||||
->with(1037, 1337);
|
||||
->with(437, 1337);
|
||||
|
||||
$this->time->method('getTime')
|
||||
->willReturn(1337);
|
||||
|
||||
@@ -243,6 +243,7 @@ class UserStatusControllerTest extends TestCase {
|
||||
* @param Throwable|null $exception
|
||||
* @param bool $expectLogger
|
||||
* @param string|null $expectedLogMessage
|
||||
* @param bool $expectSuccessAsReset
|
||||
*
|
||||
* @dataProvider setCustomMessageDataProvider
|
||||
*/
|
||||
@@ -253,7 +254,8 @@ class UserStatusControllerTest extends TestCase {
|
||||
bool $expectException,
|
||||
?Throwable $exception,
|
||||
bool $expectLogger,
|
||||
?string $expectedLogMessage): void {
|
||||
?string $expectedLogMessage,
|
||||
bool $expectSuccessAsReset = false): void {
|
||||
$userStatus = $this->getUserStatus();
|
||||
|
||||
if ($expectException) {
|
||||
@@ -262,10 +264,25 @@ class UserStatusControllerTest extends TestCase {
|
||||
->with('john.doe', $statusIcon, $message, $clearAt)
|
||||
->willThrowException($exception);
|
||||
} else {
|
||||
$this->service->expects($this->once())
|
||||
->method('setCustomMessage')
|
||||
->with('john.doe', $statusIcon, $message, $clearAt)
|
||||
->willReturn($userStatus);
|
||||
if ($expectSuccessAsReset) {
|
||||
$this->service->expects($this->never())
|
||||
->method('setCustomMessage');
|
||||
$this->service->expects($this->once())
|
||||
->method('clearMessage')
|
||||
->with('john.doe');
|
||||
$this->service->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->with('john.doe')
|
||||
->willReturn($userStatus);
|
||||
} else {
|
||||
$this->service->expects($this->once())
|
||||
->method('setCustomMessage')
|
||||
->with('john.doe', $statusIcon, $message, $clearAt)
|
||||
->willReturn($userStatus);
|
||||
|
||||
$this->service->expects($this->never())
|
||||
->method('clearMessage');
|
||||
}
|
||||
}
|
||||
|
||||
if ($expectLogger) {
|
||||
@@ -297,6 +314,7 @@ class UserStatusControllerTest extends TestCase {
|
||||
public function setCustomMessageDataProvider(): array {
|
||||
return [
|
||||
['👨🏽💻', 'Busy developing the status feature', 500, true, false, null, false, null],
|
||||
['👨🏽💻', '', 500, true, false, null, false, null, true],
|
||||
['👨🏽💻', 'Busy developing the status feature', 500, false, true, new InvalidClearAtException('Original exception message'), true,
|
||||
'New user-status for "john.doe" was rejected due to an invalid clearAt value "500"'],
|
||||
['👨🏽💻', 'Busy developing the status feature', 500, false, true, new InvalidStatusIconException('Original exception message'), true,
|
||||
|
||||
@@ -152,7 +152,7 @@ class StatusServiceTest extends TestCase {
|
||||
$status->setIsUserDefined(true);
|
||||
|
||||
$this->timeFactory->method('getTime')
|
||||
->willReturn(1400);
|
||||
->willReturn(2600);
|
||||
$this->mapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->with('john.doe')
|
||||
@@ -160,7 +160,7 @@ class StatusServiceTest extends TestCase {
|
||||
|
||||
$this->assertEquals($status, $this->service->findByUserId('john.doe'));
|
||||
$this->assertEquals('offline', $status->getStatus());
|
||||
$this->assertEquals(1400, $status->getStatusTimestamp());
|
||||
$this->assertEquals(2600, $status->getStatusTimestamp());
|
||||
$this->assertFalse($status->getIsUserDefined());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,18 @@ const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'dashboard': path.join(__dirname, 'src', 'dashboard'),
|
||||
'user-status-menu': path.join(__dirname, 'src', 'main-user-status-menu')
|
||||
dashboard: path.join(__dirname, 'src', 'dashboard'),
|
||||
'user-status-menu': path.join(__dirname, 'src', 'main-user-status-menu'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, './js'),
|
||||
publicPath: '/js/',
|
||||
filename: '[name].js?v=[chunkhash]',
|
||||
jsonpFunction: 'webpackJsonpUserStatus'
|
||||
jsonpFunction: 'webpackJsonpUserStatus',
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
automaticNameDelimiter: '-',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
0
apps/weather_status/l10n/.gitkeep
Normal file
0
apps/weather_status/l10n/.gitkeep
Normal file
@@ -46,7 +46,7 @@ class WeatherStatusController extends OCSController {
|
||||
IRequest $request,
|
||||
ILogger $logger,
|
||||
WeatherStatusService $service,
|
||||
string $userId) {
|
||||
?string $userId) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->userId = $userId;
|
||||
$this->logger = $logger;
|
||||
|
||||
@@ -105,7 +105,7 @@ class WeatherStatusService {
|
||||
IUserManager $userManager,
|
||||
IAppManager $appManager,
|
||||
ICacheFactory $cacheFactory,
|
||||
string $userId) {
|
||||
?string $userId) {
|
||||
$this->config = $config;
|
||||
$this->userId = $userId;
|
||||
$this->l10n = $l10n;
|
||||
|
||||
@@ -240,7 +240,11 @@ export default {
|
||||
console.info('The weather status request was cancelled because the user navigates.')
|
||||
return
|
||||
}
|
||||
showError(t('weather_status', 'There was an error getting the weather status information.'))
|
||||
if (err.response && err.response.status === 401) {
|
||||
showError(t('weather_status', 'You are not logged in.'))
|
||||
} else {
|
||||
showError(t('weather_status', 'There was an error getting the weather status information.'))
|
||||
}
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
@@ -309,8 +313,11 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
} catch (err) {
|
||||
showError(t('weather_status', 'There was an error setting the location address.'))
|
||||
console.debug(err)
|
||||
if (err.response && err.response.status === 401) {
|
||||
showError(t('weather_status', 'You are not logged in.'))
|
||||
} else {
|
||||
showError(t('weather_status', 'There was an error setting the location address.'))
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
@@ -320,7 +327,11 @@ export default {
|
||||
this.address = loc.address
|
||||
this.startLoop()
|
||||
} catch (err) {
|
||||
showError(t('weather_status', 'There was an error setting the location.'))
|
||||
if (err.response && err.response.status === 401) {
|
||||
showError(t('weather_status', 'You are not logged in.'))
|
||||
} else {
|
||||
showError(t('weather_status', 'There was an error setting the location.'))
|
||||
}
|
||||
console.debug(err)
|
||||
}
|
||||
},
|
||||
@@ -328,7 +339,11 @@ export default {
|
||||
try {
|
||||
await network.setMode(mode)
|
||||
} catch (err) {
|
||||
showError(t('weather_status', 'There was an error saving the mode.'))
|
||||
if (err.response && err.response.status === 401) {
|
||||
showError(t('weather_status', 'You are not logged in.'))
|
||||
} else {
|
||||
showError(t('weather_status', 'There was an error saving the mode.'))
|
||||
}
|
||||
console.debug(err)
|
||||
}
|
||||
},
|
||||
@@ -345,7 +360,11 @@ export default {
|
||||
this.mode = MODE_MANUAL_LOCATION
|
||||
this.startLoop()
|
||||
} catch (err) {
|
||||
showError(t('weather_status', 'There was an error using personal address.'))
|
||||
if (err.response && err.response.status === 401) {
|
||||
showError(t('weather_status', 'You are not logged in.'))
|
||||
} else {
|
||||
showError(t('weather_status', 'There was an error using personal address.'))
|
||||
}
|
||||
console.debug(err)
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -27,6 +27,7 @@
|
||||
require __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class CommandLineContext implements \Behat\Behat\Context\Context {
|
||||
use CommandLine;
|
||||
@@ -129,4 +130,11 @@ class CommandLineContext implements \Behat\Behat\Context\Context {
|
||||
$davPath = rtrim($davPath, '/') . $this->lastTransferPath;
|
||||
$this->featureContext->usingDavPath($davPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^transfer folder name contains "([^"]+)"$/
|
||||
*/
|
||||
public function transferFolderNameContains($text) {
|
||||
Assert::assertContains($text, $this->lastTransferPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ trait Provisioning {
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^user "([^"]*)" with displayname "([^"]*)" exists$/
|
||||
* @Given /^user "([^"]*)" with displayname "((?:[^"]|\\")*)" exists$/
|
||||
* @param string $user
|
||||
*/
|
||||
public function assureUserWithDisplaynameExists($user, $displayname) {
|
||||
|
||||
@@ -29,6 +29,22 @@ Feature: transfer-ownership
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the folder "/test" exists
|
||||
|
||||
Scenario: transferring ownership from user with risky display name
|
||||
Given user "user0" with displayname "user0 \"risky\"? ヂspḷay 'na|\/|e':.#" exists
|
||||
And user "user1" exists
|
||||
And User "user0" created a folder "/test"
|
||||
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
|
||||
When transferring ownership from "user0" to "user1"
|
||||
And the command was successful
|
||||
And As an "user1"
|
||||
And using received transfer folder of "user1" as dav path
|
||||
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
|
||||
And transfer folder name contains "transferred from user0 -risky- ヂspḷay -na|-|e- on"
|
||||
And using old dav path
|
||||
And as "user0" the folder "/test" does not exist
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the folder "/test" exists
|
||||
|
||||
Scenario: transferring ownership of file shares
|
||||
Given user "user0" exists
|
||||
And user "user1" exists
|
||||
@@ -290,6 +306,20 @@ Feature: transfer-ownership
|
||||
Then the command error output contains the text "Unknown target user"
|
||||
And the command failed with exit code 1
|
||||
|
||||
Scenario: transferring ownership of a file
|
||||
Given user "user0" exists
|
||||
And user "user1" exists
|
||||
And User "user0" uploads file "data/textfile.txt" to "/somefile.txt"
|
||||
When transferring ownership of path "somefile.txt" from "user0" to "user1"
|
||||
And the command was successful
|
||||
And As an "user1"
|
||||
And using received transfer folder of "user1" as dav path
|
||||
Then Downloaded content when downloading file "/somefile.txt" with range "bytes=0-6" should be "This is"
|
||||
And using old dav path
|
||||
And as "user0" the file "/somefile.txt" does not exist
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the file "/somefile.txt" exists
|
||||
|
||||
Scenario: transferring ownership of a folder
|
||||
Given user "user0" exists
|
||||
And user "user1" exists
|
||||
@@ -305,6 +335,73 @@ Feature: transfer-ownership
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the folder "/test" exists
|
||||
|
||||
Scenario: transferring ownership from user with risky display name
|
||||
Given user "user0" with displayname "user0 \"risky\"? ヂspḷay 'na|\/|e':.#" exists
|
||||
And user "user1" exists
|
||||
And User "user0" created a folder "/test"
|
||||
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
|
||||
When transferring ownership of path "test" from "user0" to "user1"
|
||||
And the command was successful
|
||||
And As an "user1"
|
||||
And using received transfer folder of "user1" as dav path
|
||||
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
|
||||
And transfer folder name contains "transferred from user0 -risky- ヂspḷay -na|-|e- on"
|
||||
And using old dav path
|
||||
And as "user0" the folder "/test" does not exist
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the folder "/test" exists
|
||||
|
||||
Scenario: transferring ownership of path does not affect other files
|
||||
Given user "user0" exists
|
||||
And user "user1" exists
|
||||
And User "user0" created a folder "/test"
|
||||
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
|
||||
And User "user0" created a folder "/test2"
|
||||
And User "user0" uploads file "data/textfile.txt" to "/test2/somefile.txt"
|
||||
When transferring ownership of path "test" from "user0" to "user1"
|
||||
And the command was successful
|
||||
And As an "user1"
|
||||
And using received transfer folder of "user1" as dav path
|
||||
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
|
||||
And using old dav path
|
||||
And as "user0" the folder "/test" does not exist
|
||||
And as "user0" the folder "/test2" exists
|
||||
And as "user0" the file "/test2/somefile.txt" exists
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the folder "/test" exists
|
||||
And as "user1" the folder "/test2" does not exist
|
||||
|
||||
Scenario: transferring ownership of path does not affect other shares
|
||||
Given user "user0" exists
|
||||
And user "user1" exists
|
||||
And User "user0" created a folder "/test"
|
||||
And User "user0" uploads file "data/textfile.txt" to "/test/somefile.txt"
|
||||
And User "user0" created a folder "/test2"
|
||||
And User "user0" uploads file "data/textfile.txt" to "/test2/sharedfile.txt"
|
||||
And file "/test2/sharedfile.txt" of user "user0" is shared with user "user1" with permissions 19
|
||||
And user "user1" accepts last share
|
||||
When transferring ownership of path "test" from "user0" to "user1"
|
||||
And the command was successful
|
||||
And As an "user1"
|
||||
And using received transfer folder of "user1" as dav path
|
||||
Then Downloaded content when downloading file "/test/somefile.txt" with range "bytes=0-6" should be "This is"
|
||||
And using old dav path
|
||||
And as "user0" the folder "/test" does not exist
|
||||
And as "user0" the folder "/test2" exists
|
||||
And as "user0" the file "/test2/sharedfile.txt" exists
|
||||
And using received transfer folder of "user1" as dav path
|
||||
And as "user1" the folder "/test" exists
|
||||
And as "user1" the folder "/test2" does not exist
|
||||
And using old dav path
|
||||
And as "user1" the file "/sharedfile.txt" exists
|
||||
And As an "user1"
|
||||
And Getting info of last share
|
||||
And the OCS status code should be "100"
|
||||
And Share fields of last share match with
|
||||
| uid_owner | user0 |
|
||||
| uid_file_owner | user0 |
|
||||
| share_with | user1 |
|
||||
|
||||
Scenario: transferring ownership of file shares
|
||||
Given user "user0" exists
|
||||
And user "user1" exists
|
||||
|
||||
@@ -33,6 +33,8 @@ use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IConfig;
|
||||
use OCP\ILogger;
|
||||
use OCP\Lock\ILockingProvider;
|
||||
use OCP\Lock\LockedException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -54,11 +56,14 @@ class Repair extends Command {
|
||||
private $memoryLimit;
|
||||
/** @var int */
|
||||
private $memoryTreshold;
|
||||
/** @var ILockingProvider */
|
||||
private $lockingProvider;
|
||||
|
||||
public function __construct(IConfig $config, IRootFolder $rootFolder, ILogger $logger, IniGetWrapper $phpIni) {
|
||||
public function __construct(IConfig $config, IRootFolder $rootFolder, ILogger $logger, IniGetWrapper $phpIni, ILockingProvider $lockingProvider) {
|
||||
$this->config = $config;
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->logger = $logger;
|
||||
$this->lockingProvider = $lockingProvider;
|
||||
|
||||
$this->memoryLimit = $phpIni->getBytes('memory_limit');
|
||||
$this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024;
|
||||
@@ -95,8 +100,6 @@ class Repair extends Command {
|
||||
$output->writeln("");
|
||||
}
|
||||
|
||||
$verbose = $output->isVerbose();
|
||||
|
||||
$instanceId = $this->config->getSystemValueString('instanceid');
|
||||
|
||||
$output->writeln("This will migrate all previews from the old preview location to the new one.");
|
||||
@@ -218,14 +221,21 @@ class Repair extends Command {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$lockName = 'occ preview:repair lock ' . $oldPreviewFolder->getId();
|
||||
try {
|
||||
$section1->writeln(" Locking \"$lockName\" …", OutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->lockingProvider->acquireLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
|
||||
} catch (LockedException $e) {
|
||||
$section1->writeln(" Skipping because it is locked - another process seems to work on this …");
|
||||
continue;
|
||||
}
|
||||
|
||||
$previews = $oldPreviewFolder->getDirectoryListing();
|
||||
if ($previews !== []) {
|
||||
try {
|
||||
$this->rootFolder->get("appdata_$instanceId/preview/$newFoldername");
|
||||
} catch (NotFoundException $e) {
|
||||
if ($verbose) {
|
||||
$section1->writeln(" Create folder preview/$newFoldername");
|
||||
}
|
||||
$section1->writeln(" Create folder preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
|
||||
if (!$dryMode) {
|
||||
$this->rootFolder->newFolder("appdata_$instanceId/preview/$newFoldername");
|
||||
}
|
||||
@@ -240,9 +250,7 @@ class Repair extends Command {
|
||||
$progressBar->advance();
|
||||
continue;
|
||||
}
|
||||
if ($verbose) {
|
||||
$section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername");
|
||||
}
|
||||
$section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
|
||||
if (!$dryMode) {
|
||||
try {
|
||||
$preview->move("appdata_$instanceId/preview/$newFoldername/$previewName");
|
||||
@@ -253,9 +261,7 @@ class Repair extends Command {
|
||||
}
|
||||
}
|
||||
if ($oldPreviewFolder->getDirectoryListing() === []) {
|
||||
if ($verbose) {
|
||||
$section1->writeln(" Delete empty folder preview/$name");
|
||||
}
|
||||
$section1->writeln(" Delete empty folder preview/$name", OutputInterface::VERBOSITY_VERBOSE);
|
||||
if (!$dryMode) {
|
||||
try {
|
||||
$oldPreviewFolder->delete();
|
||||
@@ -264,6 +270,10 @@ class Repair extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->lockingProvider->releaseLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
|
||||
$section1->writeln(" Unlocked", OutputInterface::VERBOSITY_VERBOSE);
|
||||
|
||||
$section1->writeln(" Finished migrating previews of file with fileId $name …");
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
@@ -433,7 +433,7 @@ nav[role='navigation'] {
|
||||
li {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
padding: 0 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -446,6 +446,9 @@ nav[role='navigation'] {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: .6;
|
||||
// Make sure most app names don’t ellipsize
|
||||
letter-spacing: -0.5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* focused app visual feedback */
|
||||
@@ -453,13 +456,21 @@ nav[role='navigation'] {
|
||||
a:focus,
|
||||
a.active {
|
||||
opacity: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Text size back to normal for hover/focus
|
||||
&:hover a,
|
||||
a:focus {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover a + span,
|
||||
a:focus + span,
|
||||
&:hover span,
|
||||
&:focus span,
|
||||
a:focus span {
|
||||
a:focus span,
|
||||
a.active span {
|
||||
display: inline-block;
|
||||
text-overflow: initial;
|
||||
width: auto;
|
||||
@@ -482,7 +493,7 @@ nav[role='navigation'] {
|
||||
position: absolute;
|
||||
color: var(--color-primary-text);
|
||||
bottom: 2px;
|
||||
width: calc(100% - 4px);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user