feat(files): Quota in navigation

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
pull/36002/head
John Molakvoæ 1 year ago
parent 9af7ee8d11
commit b9906fb21e
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF

@ -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'

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

@ -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);

18
package-lock.json generated

@ -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",

Loading…
Cancel
Save