Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v26.0.0beta1
@@ -8,8 +8,15 @@ module.exports = { | |||
oc_userconfig: true, | |||
dayNames: true, | |||
firstDay: true, | |||
'cypress/globals': true, | |||
}, | |||
extends: ['@nextcloud'], | |||
plugins: [ | |||
'cypress', | |||
], | |||
extends: [ | |||
'@nextcloud', | |||
'plugin:cypress/recommended', | |||
], | |||
rules: { | |||
'no-tabs': 'warn', | |||
// TODO: make sure we fix this as this is bad vue coding style. |
@@ -61,11 +61,6 @@ $application->registerRoutes( | |||
'verb' => 'GET', | |||
'root' => '', | |||
], | |||
[ | |||
'name' => 'ajax#getStorageStats', | |||
'url' => '/ajax/getstoragestats', | |||
'verb' => 'GET', | |||
], | |||
[ | |||
'name' => 'API#getThumbnail', | |||
'url' => '/api/v1/thumbnail/{x}/{y}/{file}', | |||
@@ -83,6 +78,11 @@ $application->registerRoutes( | |||
'url' => '/api/v1/recent/', | |||
'verb' => 'GET' | |||
], | |||
[ | |||
'name' => 'API#getStorageStats', | |||
'url' => '/api/v1/stats', | |||
'verb' => 'GET' | |||
], | |||
[ | |||
'name' => 'API#setConfig', | |||
'url' => '/api/v1/config/{key}', |
@@ -32,7 +32,6 @@ return array( | |||
'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php', | |||
'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php', | |||
'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php', | |||
'OCA\\Files\\Controller\\AjaxController' => $baseDir . '/../lib/Controller/AjaxController.php', | |||
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', | |||
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php', | |||
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php', |
@@ -47,7 +47,6 @@ class ComposerStaticInitFiles | |||
'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php', | |||
'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php', | |||
'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php', | |||
'OCA\\Files\\Controller\\AjaxController' => __DIR__ . '/..' . '/../lib/Controller/AjaxController.php', | |||
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', | |||
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php', | |||
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php', |
@@ -1,57 +0,0 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> | |||
* | |||
* @author Roeland Jago Douma <roeland@famdouma.nl> | |||
* | |||
* @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\Files\Controller; | |||
use OCA\Files\Helper; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\Files\NotFoundException; | |||
use OCP\IRequest; | |||
class AjaxController extends Controller { | |||
public function __construct(string $appName, IRequest $request) { | |||
parent::__construct($appName, $request); | |||
} | |||
/** | |||
* @NoAdminRequired | |||
*/ | |||
public function getStorageStats(string $dir = '/'): JSONResponse { | |||
try { | |||
return new JSONResponse([ | |||
'status' => 'success', | |||
'data' => Helper::buildFileStorageStatistics($dir), | |||
]); | |||
} catch (NotFoundException $e) { | |||
return new JSONResponse([ | |||
'status' => 'error', | |||
'data' => [ | |||
'message' => 'Folder not found' | |||
], | |||
]); | |||
} | |||
} | |||
} |
@@ -257,6 +257,20 @@ class ApiController extends Controller { | |||
return new DataResponse(['files' => $files]); | |||
} | |||
/** | |||
* Returns the current logged-in user's storage stats. | |||
* | |||
* @NoAdminRequired | |||
* | |||
* @param ?string $dir the directory to get the storage stats from | |||
* @return JSONResponse | |||
*/ | |||
public function getStorageStats($dir = '/'): JSONResponse { | |||
$storageInfo = \OC_Helper::getStorageInfo($dir ?: '/'); | |||
return new JSONResponse(['message' => 'ok', 'data' => $storageInfo]); | |||
} | |||
/** | |||
* Change the default sort mode | |||
* |
@@ -136,11 +136,11 @@ class ViewController extends Controller { | |||
* @return array | |||
* @throws \OCP\Files\NotFoundException | |||
*/ | |||
protected function getStorageInfo() { | |||
protected function getStorageInfo(string $dir = '/') { | |||
\OC_Util::setupFS(); | |||
$dirInfo = \OC\Files\Filesystem::getFileInfo('/', false); | |||
$rootInfo = \OC\Files\Filesystem::getFileInfo('/', false); | |||
return \OC_Helper::getStorageInfo('/', $dirInfo); | |||
return \OC_Helper::getStorageInfo($dir, $rootInfo ?: null); | |||
} | |||
/** | |||
@@ -241,18 +241,16 @@ class ViewController extends Controller { | |||
$nav->assign('navigationItems', $navItems); | |||
$nav->assign('usage', \OC_Helper::humanFileSize($storageInfo['used'])); | |||
if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { | |||
$totalSpace = $this->l10n->t('Unlimited'); | |||
} else { | |||
$totalSpace = \OC_Helper::humanFileSize($storageInfo['total']); | |||
} | |||
$nav->assign('total_space', $totalSpace); | |||
$nav->assign('quota', $storageInfo['quota']); | |||
$nav->assign('usage_relative', $storageInfo['relative']); | |||
$contentItems = []; | |||
try { | |||
// If view is files, we use the directory, otherwise we use the root storage | |||
$storageInfo = $this->getStorageInfo(($view === 'files' && $dir) ? $dir : '/'); | |||
} catch(\Exception $e) { | |||
$storageInfo = $this->getStorageInfo(); | |||
} | |||
$this->initialState->provideInitialState('storageStats', $storageInfo); | |||
$this->initialState->provideInitialState('navigation', $navItems); | |||
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs()); | |||
@@ -0,0 +1,153 @@ | |||
<template> | |||
<NcAppNavigationItem v-if="storageStats" | |||
:aria-label="t('files', 'Storage informations')" | |||
:class="{ 'app-navigation-entry__settings-quota--not-unlimited': storageStats.quota >= 0}" | |||
:loading="loadingStorageStats" | |||
:name="storageStatsTitle" | |||
:title="storageStatsTooltip" | |||
class="app-navigation-entry__settings-quota" | |||
data-cy-files-navigation-settings-quota | |||
@click.stop.prevent="debounceUpdateStorageStats"> | |||
<ChartPie slot="icon" :size="20" /> | |||
<!-- Progress bar --> | |||
<NcProgressBar v-if="storageStats.quota >= 0" | |||
slot="extra" | |||
:error="storageStats.relative > 80" | |||
:value="Math.min(storageStats.relative, 100)" /> | |||
</NcAppNavigationItem> | |||
</template> | |||
<script> | |||
import { formatFileSize } from '@nextcloud/files' | |||
import { generateUrl } from '@nextcloud/router' | |||
import { loadState } from '@nextcloud/initial-state' | |||
import { showError } from '@nextcloud/dialogs' | |||
import { debounce, throttle } from 'throttle-debounce' | |||
import { translate } from '@nextcloud/l10n' | |||
import axios from '@nextcloud/axios' | |||
import ChartPie from 'vue-material-design-icons/ChartPie.vue' | |||
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' | |||
import NcProgressBar from '@nextcloud/vue/dist/Components/NcProgressBar.js' | |||
import logger from '../logger.js' | |||
import { subscribe } from '@nextcloud/event-bus' | |||
export default { | |||
name: 'NavigationQuota', | |||
components: { | |||
ChartPie, | |||
NcAppNavigationItem, | |||
NcProgressBar, | |||
}, | |||
data() { | |||
return { | |||
loadingStorageStats: false, | |||
storageStats: loadState('files', 'storageStats', null), | |||
} | |||
}, | |||
computed: { | |||
storageStatsTitle() { | |||
const usedQuotaByte = formatFileSize(this.storageStats?.used) | |||
const quotaByte = formatFileSize(this.storageStats?.quota) | |||
// If no quota set | |||
if (this.storageStats?.quota < 0) { | |||
return this.t('files', '{usedQuotaByte} used', { usedQuotaByte }) | |||
} | |||
return this.t('files', '{used} of {quota} used', { | |||
used: usedQuotaByte, | |||
quota: quotaByte, | |||
}) | |||
}, | |||
storageStatsTooltip() { | |||
if (!this.storageStats.relative) { | |||
return '' | |||
} | |||
return this.t('files', '{relative}% used', this.storageStats) | |||
}, | |||
}, | |||
beforeMount() { | |||
/** | |||
* Update storage stats every minute | |||
* TODO: remove when all views are migrated to Vue | |||
*/ | |||
setInterval(this.throttleUpdateStorageStats, 60 * 1000) | |||
subscribe('files:file:created', this.throttleUpdateStorageStats) | |||
subscribe('files:file:deleted', this.throttleUpdateStorageStats) | |||
subscribe('files:file:moved', this.throttleUpdateStorageStats) | |||
subscribe('files:file:updated', this.throttleUpdateStorageStats) | |||
subscribe('files:folder:created', this.throttleUpdateStorageStats) | |||
subscribe('files:folder:deleted', this.throttleUpdateStorageStats) | |||
subscribe('files:folder:moved', this.throttleUpdateStorageStats) | |||
subscribe('files:folder:updated', this.throttleUpdateStorageStats) | |||
}, | |||
methods: { | |||
// From user input | |||
debounceUpdateStorageStats: debounce(200, function(event) { | |||
this.updateStorageStats(event) | |||
}), | |||
// From interval or event bus | |||
throttleUpdateStorageStats: throttle(1000, function(event) { | |||
this.updateStorageStats(event) | |||
}), | |||
/** | |||
* Update the storage stats | |||
* Throttled at max 1 refresh per minute | |||
* | |||
* @param {Event} [event = null] if user interaction | |||
*/ | |||
async updateStorageStats(event = null) { | |||
if (this.loadingStorageStats) { | |||
return | |||
} | |||
this.loadingStorageStats = true | |||
try { | |||
const response = await axios.get(generateUrl('/apps/files/api/v1/stats')) | |||
if (!response?.data?.data) { | |||
throw new Error('Invalid storage stats') | |||
} | |||
this.storageStats = response.data.data | |||
} catch (error) { | |||
logger.error('Could not refresh storage stats', { error }) | |||
// Only show to the user if it was manually triggered | |||
if (event) { | |||
showError(t('files', 'Could not refresh storage stats')) | |||
} | |||
} finally { | |||
this.loadingStorageStats = false | |||
} | |||
}, | |||
t: translate, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
// User storage stats display | |||
.app-navigation-entry__settings-quota { | |||
// Align title with progress and icon | |||
&--not-unlimited::v-deep .app-navigation-entry__title { | |||
margin-top: -4px; | |||
} | |||
progress { | |||
position: absolute; | |||
bottom: 10px; | |||
margin-left: 44px; | |||
width: calc(100% - 44px - 22px); | |||
} | |||
} | |||
</style> |
@@ -1,4 +1,5 @@ | |||
/* eslint-disable import/first */ | |||
import * as InitialState from '@nextcloud/initial-state' | |||
import * as L10n from '@nextcloud/l10n' | |||
import FolderSvg from '@mdi/svg/svg/folder.svg' | |||
import ShareSvg from '@mdi/svg/svg/share-variant.svg' | |||
@@ -6,9 +7,18 @@ import NavigationService from '../services/Navigation' | |||
import NavigationView from './Navigation.vue' | |||
import router from '../router/router.js' | |||
const Navigation = new NavigationService() | |||
describe('Navigation renders', () => { | |||
const Navigation = new NavigationService() | |||
before(() => { | |||
cy.stub(InitialState, 'loadState') | |||
.returns({ | |||
used: 1024 * 1024 * 1024, | |||
quota: -1, | |||
}) | |||
}) | |||
it('renders', () => { | |||
cy.mount(NavigationView, { | |||
propsData: { | |||
@@ -17,11 +27,14 @@ describe('Navigation renders', () => { | |||
}) | |||
cy.get('[data-cy-files-navigation]').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-button]').should('be.visible') | |||
}) | |||
}) | |||
describe('Navigation API', () => { | |||
const Navigation = new NavigationService() | |||
it('Check API entries rendering', () => { | |||
Navigation.register({ | |||
id: 'files', | |||
@@ -114,3 +127,93 @@ describe('Navigation API', () => { | |||
}).to.throw('Navigation id files is already registered') | |||
}) | |||
}) | |||
describe('Quota rendering', () => { | |||
const Navigation = new NavigationService() | |||
beforeEach(() => { | |||
// TODO: remove when @nextcloud/l10n 2.0 is released | |||
// https://github.com/nextcloud/nextcloud-l10n/pull/542 | |||
cy.stub(L10n, 'translate', (app, text, vars = {}, number) => { | |||
cy.log({app, text, vars, number}) | |||
return text.replace(/%n/g, '' + number).replace(/{([^{}]*)}/g, (match, key) => { | |||
return vars[key] | |||
}) | |||
}) | |||
}) | |||
it('Unknown quota', () => { | |||
cy.stub(InitialState, 'loadState') | |||
.as('loadStateStats') | |||
.returns(undefined) | |||
cy.mount(NavigationView, { | |||
propsData: { | |||
Navigation, | |||
}, | |||
}) | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('not.exist') | |||
}) | |||
it('Unlimited quota', () => { | |||
cy.stub(InitialState, 'loadState') | |||
.as('loadStateStats') | |||
.returns({ | |||
used: 1024 * 1024 * 1024, | |||
quota: -1, | |||
}) | |||
cy.mount(NavigationView, { | |||
propsData: { | |||
Navigation, | |||
}, | |||
}) | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('contain.text', '1 GB used') | |||
cy.get('[data-cy-files-navigation-settings-quota] progress').should('not.exist') | |||
}) | |||
it('Non-reached quota', () => { | |||
cy.stub(InitialState, 'loadState') | |||
.as('loadStateStats') | |||
.returns({ | |||
used: 1024 * 1024 * 1024, | |||
quota: 5 * 1024 * 1024 * 1024, | |||
relative: 20, // percent | |||
}) | |||
cy.mount(NavigationView, { | |||
propsData: { | |||
Navigation, | |||
}, | |||
}) | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('contain.text', '1 GB of 5 GB used') | |||
cy.get('[data-cy-files-navigation-settings-quota] progress').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-quota] progress').should('have.attr', 'value', '20') | |||
}) | |||
it('Reached quota', () => { | |||
cy.stub(InitialState, 'loadState') | |||
.as('loadStateStats') | |||
.returns({ | |||
used: 5 * 1024 * 1024 * 1024, | |||
quota: 1024 * 1024 * 1024, | |||
relative: 500, // percent | |||
}) | |||
cy.mount(NavigationView, { | |||
propsData: { | |||
Navigation, | |||
}, | |||
}) | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-quota]').should('contain.text', '5 GB of 1 GB used') | |||
cy.get('[data-cy-files-navigation-settings-quota] progress').should('be.visible') | |||
cy.get('[data-cy-files-navigation-settings-quota] progress').should('have.attr', 'value', '100') // progress max is 100 | |||
}) | |||
}) |
@@ -42,10 +42,14 @@ | |||
</NcAppNavigationItem> | |||
</template> | |||
<!-- Settings toggle --> | |||
<!-- Non-scrollable navigation bottom elements --> | |||
<template #footer> | |||
<ul class="app-navigation-entry__settings"> | |||
<NcAppNavigationItem :aria-label="t('files', 'Open the Files app settings')" | |||
<!-- User storage usage statistics --> | |||
<NavigationQuota /> | |||
<!-- Files settings modal toggle--> | |||
<NcAppNavigationItem :aria-label="t('files', 'Open the files app settings')" | |||
:title="t('files', 'Files settings')" | |||
data-cy-files-navigation-settings-button | |||
@click.prevent.stop="openSettings"> | |||
@@ -64,6 +68,8 @@ | |||
<script> | |||
import { emit, subscribe } from '@nextcloud/event-bus' | |||
import { generateUrl } from '@nextcloud/router' | |||
import { translate } from '@nextcloud/l10n' | |||
import axios from '@nextcloud/axios' | |||
import Cog from 'vue-material-design-icons/Cog.vue' | |||
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' | |||
@@ -71,10 +77,9 @@ import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationI | |||
import logger from '../logger.js' | |||
import Navigation from '../services/Navigation.ts' | |||
import NavigationQuota from '../components/NavigationQuota.vue' | |||
import SettingsModal from './Settings.vue' | |||
import { translate } from '@nextcloud/l10n' | |||
export default { | |||
name: 'Navigation', | |||
@@ -83,6 +88,7 @@ export default { | |||
NcAppNavigation, | |||
NcAppNavigationItem, | |||
SettingsModal, | |||
NavigationQuota, | |||
}, | |||
props: { | |||
@@ -103,6 +109,8 @@ export default { | |||
currentViewId() { | |||
return this.$route?.params?.view || 'files' | |||
}, | |||
/** @return {Navigation} */ | |||
currentView() { | |||
return this.views.find(view => view.id === this.currentViewId) | |||
}, | |||
@@ -111,6 +119,8 @@ export default { | |||
views() { | |||
return this.Navigation.views | |||
}, | |||
/** @return {Navigation[]} */ | |||
parentViews() { | |||
return this.views | |||
// filter child views | |||
@@ -120,6 +130,8 @@ export default { | |||
return a.order - b.order | |||
}) | |||
}, | |||
/** @return {Navigation[]} */ | |||
childViews() { | |||
return this.views | |||
// filter parent views | |||
@@ -213,6 +225,7 @@ export default { | |||
/** | |||
* Generate the route to a view | |||
* | |||
* @param {Navigation} view the view to toggle | |||
*/ | |||
generateToNavigation(view) { |
@@ -285,6 +285,13 @@ export default { | |||
return OCA && 'SystemTags' in OCA | |||
}, | |||
}, | |||
created() { | |||
window.addEventListener('resize', this.handleWindowResize) | |||
this.handleWindowResize() | |||
}, | |||
beforeDestroy() { | |||
window.removeEventListener('resize', this.handleWindowResize) | |||
}, | |||
methods: { | |||
/** | |||
@@ -494,13 +501,6 @@ export default { | |||
this.hasLowHeight = document.documentElement.clientHeight < 1024 | |||
}, | |||
}, | |||
created() { | |||
window.addEventListener('resize', this.handleWindowResize) | |||
this.handleWindowResize() | |||
}, | |||
beforeDestroy() { | |||
window.removeEventListener('resize', this.handleWindowResize) | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> |
@@ -9,51 +9,7 @@ | |||
$pinned = NavigationListElements($item, $l, $pinned); | |||
} | |||
?> | |||
<?php if ($_['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED): ?> | |||
<li id="quota" class="pinned <?php p($pinned === 0 ? 'first-pinned ' : '') ?>"> | |||
<a href="#" class="icon-quota svg quota-navigation-item"> | |||
<p id="quotatext" class="quota-navigation-item__text"><?php p($l->t('%s used', [$_['usage']])); ?></p> | |||
</a> | |||
</li> | |||
<?php else: ?> | |||
<li id="quota" class="has-tooltip pinned <?php p($pinned === 0 ? 'first-pinned ' : '') ?>" | |||
title="<?php p($l->t('%s%%', [round($_['usage_relative'])])); ?>"> | |||
<a href="#" class="icon-quota svg quota-navigation-item"> | |||
<p id="quotatext" class="quota-navigation-item__text"><?php p($l->t('%1$s of %2$s used', [$_['usage'], $_['total_space']])); ?></p> | |||
<div class="quota-navigation-item__container"> | |||
<progress value="<?php p($_['usage_relative']); ?>" max="100" class="<?= ($_['usage_relative'] > 80) ? 'warn' : '' ?>"></progress> | |||
</div> | |||
</a> | |||
</li> | |||
<?php endif; ?> | |||
</ul> | |||
<div id="app-settings"> | |||
<div id="app-settings-header"> | |||
<button class="settings-button" | |||
data-apps-slide-toggle="#app-settings-content"> | |||
<?php p($l->t('Files settings')); ?> | |||
</button> | |||
</div> | |||
<div id="app-settings-content"> | |||
<div id="files-app-settings"></div> | |||
<div id="files-setting-showhidden"> | |||
<input class="checkbox" id="showhiddenfilesToggle" | |||
checked="checked" type="checkbox"> | |||
<label for="showhiddenfilesToggle"><?php p($l->t('Show hidden files')); ?></label> | |||
</div> | |||
<div id="files-setting-cropimagepreviews"> | |||
<input class="checkbox" id="cropimagepreviewsToggle" | |||
checked="checked" type="checkbox"> | |||
<label for="cropimagepreviewsToggle"><?php p($l->t('Crop image previews')); ?></label> | |||
</div> | |||
<label for="webdavurl"><?php p($l->t('WebDAV')); ?></label> | |||
<input id="webdavurl" type="text" readonly="readonly" | |||
value="<?php p($_['webdav_url']); ?>"/> | |||
<em><a href="<?php echo link_to_docs('user-webdav') ?>" target="_blank" rel="noreferrer noopener"><?php p($l->t('Use this address to access your Files via WebDAV')) ?> ↗</a></em> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -139,7 +139,7 @@ class ViewControllerTest extends TestCase { | |||
public function testIndexWithRegularBrowser() { | |||
$this->viewController | |||
->expects($this->once()) | |||
->expects($this->any()) | |||
->method('getStorageInfo') | |||
->willReturn([ | |||
'used' => 123, | |||
@@ -160,17 +160,13 @@ class ViewControllerTest extends TestCase { | |||
]); | |||
$this->config | |||
->expects($this->any()) | |||
->method('getAppValue') | |||
->willReturnArgument(2); | |||
->expects($this->any()) | |||
->method('getAppValue') | |||
->willReturnArgument(2); | |||
$this->shareManager->method('shareApiAllowLinks') | |||
->willReturn(true); | |||
$nav = new Template('files', 'appnavigation'); | |||
$nav->assign('usage_relative', 123); | |||
$nav->assign('usage', '123 B'); | |||
$nav->assign('quota', 100); | |||
$nav->assign('total_space', '100 B'); | |||
$nav->assign('navigationItems', [ | |||
'files' => [ | |||
'id' => 'files', |
@@ -100,6 +100,20 @@ export default defineConfig({ | |||
process.env.npm_package_version = '1.0.0' | |||
process.env.NODE_ENV = 'development' | |||
/** | |||
* Needed for cypress stubbing | |||
* | |||
* @see https://github.com/sinonjs/sinon/issues/1121 | |||
* @see https://github.com/cypress-io/cypress/issues/18662 | |||
*/ | |||
const babel = require('./babel.config.js') | |||
babel.plugins.push([ | |||
'@babel/plugin-transform-modules-commonjs', | |||
{ | |||
loose: true, | |||
}, | |||
]) | |||
const config = require('@nextcloud/webpack-vue-config') | |||
config.module.rules.push({ | |||
test: /\.svg$/, |
@@ -20,7 +20,8 @@ | |||
* | |||
*/ | |||
/* eslint-disable no-console */ | |||
/* eslint-disable node/no-unpublished-import */ | |||
/* eslint-disable n/no-unpublished-import */ | |||
/* eslint-disable n/no-extraneous-import */ | |||
import Docker from 'dockerode' | |||
import waitOn from 'wait-on' | |||
@@ -36,7 +37,7 @@ const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server' | |||
* | |||
* @param {string} branch the branch of your current work | |||
*/ | |||
export const startNextcloud = async function(branch: string = 'master'): Promise<any> { | |||
export const startNextcloud = async function(branch = 'master'): Promise<any> { | |||
try { | |||
// Pulling images | |||
@@ -48,6 +49,10 @@ export const startNextcloud = async function(branch: string = 'master'): Promise | |||
// https://github.com/apocas/dockerode/issues/357 | |||
docker.modem.followProgress(stream, onFinished) | |||
/** | |||
* | |||
* @param err | |||
*/ | |||
function onFinished(err) { | |||
if (!err) { | |||
resolve(true) | |||
@@ -85,7 +90,7 @@ export const startNextcloud = async function(branch: string = 'master'): Promise | |||
}, | |||
Env: [ | |||
`BRANCH=${branch}`, | |||
] | |||
], | |||
}) | |||
await container.start() | |||
@@ -19,6 +19,7 @@ | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
/* eslint-disable n/no-unpublished-import */ | |||
import { User } from '@nextcloud/cypress' | |||
import { colord } from 'colord' | |||
@@ -66,7 +67,7 @@ describe('Change the primary colour and reset it', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
pickRandomColor('[data-admin-theming-setting-primary-color-picker]') | |||
.then(color => selectedColor = color) | |||
.then(color => { selectedColor = color }) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => validateBodyThemingCss(selectedColor, defaultBackground)) | |||
@@ -310,7 +311,7 @@ describe('User default option matches admin theming', function() { | |||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') | |||
pickRandomColor('[data-admin-theming-setting-primary-color-picker]') | |||
.then(color => selectedColor = color) | |||
.then(color => { selectedColor = color }) | |||
cy.wait('@setColor') | |||
cy.waitUntil(() => cy.window().then((win) => { |
@@ -67,9 +67,9 @@ export const pickRandomColor = function(pickerSelector: string): Cypress.Chainab | |||
cy.get(pickerSelector).click() | |||
// Return selected colour | |||
return cy.get(pickerSelector).get(`.color-picker__simple-color-circle`).eq(randColour) | |||
return cy.get(pickerSelector).get('.color-picker__simple-color-circle').eq(randColour) | |||
.click().then(colorElement => { | |||
const selectedColor = colorElement.css('background-color') | |||
return selectedColor | |||
}) | |||
} | |||
} |
@@ -21,11 +21,11 @@ | |||
*/ | |||
import type { User } from '@nextcloud/cypress' | |||
import { pickRandomColor, validateBodyThemingCss } from './themingUtils' | |||
const defaultPrimary = '#006aa3' | |||
const defaultBackground = 'kamil-porembinski-clouds.jpg' | |||
import { pickRandomColor, validateBodyThemingCss } from './themingUtils' | |||
describe('User default background settings', function() { | |||
before(function() { | |||
cy.createRandomUser().then((user: User) => { |
@@ -19,7 +19,7 @@ | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
/* eslint-disable node/no-unpublished-import */ | |||
/* eslint-disable n/no-unpublished-import */ | |||
import axios from '@nextcloud/axios' | |||
import { addCommands, User } from '@nextcloud/cypress' | |||
import { basename } from 'path' | |||
@@ -105,7 +105,7 @@ Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'ima | |||
/** | |||
* Reset the admin theming entirely | |||
*/ | |||
Cypress.Commands.add('resetAdminTheming', () => { | |||
Cypress.Commands.add('resetAdminTheming', () => { | |||
const admin = new User('admin', 'admin') | |||
cy.clearCookies() | |||
@@ -119,7 +119,7 @@ Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'ima | |||
method: 'POST', | |||
url: '/index.php/apps/theming/ajax/undoAllChanges', | |||
headers: { | |||
'requesttoken': requestToken, | |||
requesttoken: requestToken, | |||
}, | |||
}) | |||
}) | |||
@@ -147,7 +147,7 @@ Cypress.Commands.add('resetUserTheming', (user?: User) => { | |||
method: 'POST', | |||
url: '/apps/theming/background/default', | |||
headers: { | |||
'requesttoken': requestToken, | |||
requesttoken: requestToken, | |||
}, | |||
}) | |||
}) |
@@ -21,15 +21,37 @@ | |||
*/ | |||
import { mount } from 'cypress/vue2' | |||
type MountParams = Parameters<typeof mount>; | |||
type OptionsParam = MountParams[1]; | |||
// Augment the Cypress namespace to include type definitions for | |||
// your custom command. | |||
// Alternatively, can be defined in cypress/support/component.d.ts | |||
// with a <reference path="./component" /> at the top of your spec. | |||
declare global { | |||
// eslint-disable-next-line @typescript-eslint/no-namespace | |||
namespace Cypress { | |||
interface Chainable<Subject = any> { | |||
mount: typeof mount; | |||
interface Chainable { | |||
mount: typeof mount | |||
} | |||
} | |||
} | |||
Cypress.Commands.add('mount', mount); | |||
// Example use: | |||
// cy.mount(MyComponent) | |||
Cypress.Commands.add('mount', (component, optionsOrProps) => { | |||
let instance = null | |||
const oldMounted = component?.mounted || false | |||
// Override the mounted method to expose | |||
// the component instance to cypress | |||
component.mounted = function() { | |||
// eslint-disable-next-line | |||
instance = this | |||
if (oldMounted) { | |||
oldMounted() | |||
} | |||
} | |||
// Expose the component with cy.get('@component') | |||
return mount(component, optionsOrProps).then(() => { | |||
return cy.wrap(instance).as('component') | |||
}) | |||
}) |
@@ -19,4 +19,4 @@ | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
import './commands' | |||
import './commands' |
@@ -470,7 +470,12 @@ class OC_Helper { | |||
// return storage info without adding mount points | |||
$includeExtStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); | |||
$fullPath = Filesystem::getView()->getAbsolutePath($path); | |||
$view = Filesystem::getView(); | |||
if (!$view) { | |||
throw new \OCP\Files\NotFoundException(); | |||
} | |||
$fullPath = $view->getAbsolutePath($path); | |||
$cacheKey = $fullPath. '::' . ($includeMountPoints ? 'include' : 'exclude'); | |||
if ($useCache) { | |||
$cached = $memcache->get($cacheKey); |
@@ -41,7 +41,7 @@ | |||
"clipboard": "^2.0.11", | |||
"colord": "^2.9.3", | |||
"core-js": "^3.24.0", | |||
"davclient.js": "git+https://github.com/owncloud/davclient.js.git#0.2.1", | |||
"davclient.js": "github:owncloud/davclient.js.git#0.2.1", | |||
"debounce": "^1.2.1", | |||
"dompurify": "^2.3.6", | |||
"escape-html": "^1.0.3", | |||
@@ -69,6 +69,7 @@ | |||
"snap.js": "^2.0.9", | |||
"stream-browserify": "^3.0.0", | |||
"strengthify": "github:nextcloud/strengthify#0.5.9", | |||
"throttle-debounce": "^5.0.0", | |||
"underscore": "1.13.4", | |||
"url-search-params-polyfill": "^8.1.1", | |||
"v-click-outside": "^3.2.0", | |||
@@ -23260,6 +23261,14 @@ | |||
"dev": true, | |||
"peer": true | |||
}, | |||
"node_modules/throttle-debounce": { | |||
"version": "5.0.0", | |||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", | |||
"integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", | |||
"engines": { | |||
"node": ">=12.22" | |||
} | |||
}, | |||
"node_modules/throttleit": { | |||
"version": "1.0.0", | |||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", | |||
@@ -32590,7 +32599,7 @@ | |||
}, | |||
"davclient.js": { | |||
"version": "git+ssh://git@github.com/owncloud/davclient.js.git#1ab200d099a3c2cd2ef919c3a56353ce26865994", | |||
"from": "davclient.js@git+https://github.com/owncloud/davclient.js.git#0.2.1" | |||
"from": "davclient.js@github:owncloud/davclient.js.git#0.2.1" | |||
}, | |||
"dayjs": { | |||
"version": "1.11.6", | |||
@@ -43232,6 +43241,11 @@ | |||
"dev": true, | |||
"peer": true | |||
}, | |||
"throttle-debounce": { | |||
"version": "5.0.0", | |||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", | |||
"integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==" | |||
}, | |||
"throttleit": { | |||
"version": "1.0.0", | |||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", |
@@ -66,7 +66,7 @@ | |||
"clipboard": "^2.0.11", | |||
"colord": "^2.9.3", | |||
"core-js": "^3.24.0", | |||
"davclient.js": "git+https://github.com/owncloud/davclient.js.git#0.2.1", | |||
"davclient.js": "github:owncloud/davclient.js.git#0.2.1", | |||
"debounce": "^1.2.1", | |||
"dompurify": "^2.3.6", | |||
"escape-html": "^1.0.3", | |||
@@ -94,6 +94,7 @@ | |||
"snap.js": "^2.0.9", | |||
"stream-browserify": "^3.0.0", | |||
"strengthify": "github:nextcloud/strengthify#0.5.9", | |||
"throttle-debounce": "^5.0.0", | |||
"underscore": "1.13.4", | |||
"url-search-params-polyfill": "^8.1.1", | |||
"v-click-outside": "^3.2.0", |