Browse Source

feat(files): Quota in navigation

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
tags/v26.0.0beta1
John Molakvoæ 1 year ago
parent
commit
b9906fb21e
No account linked to committer's email address

+ 8
- 1
.eslintrc.js View File

oc_userconfig: true, oc_userconfig: true,
dayNames: true, dayNames: true,
firstDay: true, firstDay: true,
'cypress/globals': true,
}, },
extends: ['@nextcloud'],
plugins: [
'cypress',
],
extends: [
'@nextcloud',
'plugin:cypress/recommended',
],
rules: { rules: {
'no-tabs': 'warn', 'no-tabs': 'warn',
// TODO: make sure we fix this as this is bad vue coding style. // TODO: make sure we fix this as this is bad vue coding style.

+ 5
- 5
apps/files/appinfo/routes.php View File

'verb' => 'GET', 'verb' => 'GET',
'root' => '', 'root' => '',
], ],
[
'name' => 'ajax#getStorageStats',
'url' => '/ajax/getstoragestats',
'verb' => 'GET',
],
[ [
'name' => 'API#getThumbnail', 'name' => 'API#getThumbnail',
'url' => '/api/v1/thumbnail/{x}/{y}/{file}', 'url' => '/api/v1/thumbnail/{x}/{y}/{file}',
'url' => '/api/v1/recent/', 'url' => '/api/v1/recent/',
'verb' => 'GET' 'verb' => 'GET'
], ],
[
'name' => 'API#getStorageStats',
'url' => '/api/v1/stats',
'verb' => 'GET'
],
[ [
'name' => 'API#setConfig', 'name' => 'API#setConfig',
'url' => '/api/v1/config/{key}', 'url' => '/api/v1/config/{key}',

+ 0
- 1
apps/files/composer/composer/autoload_classmap.php View File

'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php', 'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php',
'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php',
'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.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\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',

+ 0
- 1
apps/files/composer/composer/autoload_static.php View File

'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php', 'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php',
'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php',
'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.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\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',

+ 0
- 57
apps/files/lib/Controller/AjaxController.php View File

<?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'
],
]);
}
}
}

+ 14
- 0
apps/files/lib/Controller/ApiController.php View File

return new DataResponse(['files' => $files]); 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 * Change the default sort mode
* *

+ 11
- 13
apps/files/lib/Controller/ViewController.php View File

* @return array * @return array
* @throws \OCP\Files\NotFoundException * @throws \OCP\Files\NotFoundException
*/ */
protected function getStorageInfo() {
protected function getStorageInfo(string $dir = '/') {
\OC_Util::setupFS(); \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);
} }


/** /**


$nav->assign('navigationItems', $navItems); $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 = []; $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('navigation', $navItems);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs()); $this->initialState->provideInitialState('config', $this->userConfig->getConfigs());



+ 153
- 0
apps/files/src/components/NavigationQuota.vue View File

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

+ 106
- 3
apps/files/src/views/Navigation.cy.ts View File

/* 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 FolderSvg from '@mdi/svg/svg/folder.svg'
import ShareSvg from '@mdi/svg/svg/share-variant.svg' import ShareSvg from '@mdi/svg/svg/share-variant.svg'


import NavigationView from './Navigation.vue' import NavigationView from './Navigation.vue'
import router from '../router/router.js' import router from '../router/router.js'


const Navigation = new NavigationService()

describe('Navigation renders', () => { describe('Navigation renders', () => {
const Navigation = new NavigationService()

before(() => {
cy.stub(InitialState, 'loadState')
.returns({
used: 1024 * 1024 * 1024,
quota: -1,
})

})

it('renders', () => { it('renders', () => {
cy.mount(NavigationView, { cy.mount(NavigationView, {
propsData: { propsData: {
}) })


cy.get('[data-cy-files-navigation]').should('be.visible') 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') cy.get('[data-cy-files-navigation-settings-button]').should('be.visible')
}) })
}) })


describe('Navigation API', () => { describe('Navigation API', () => {
const Navigation = new NavigationService()

it('Check API entries rendering', () => { it('Check API entries rendering', () => {
Navigation.register({ Navigation.register({
id: 'files', id: 'files',
}).to.throw('Navigation id files is already registered') }).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
})
})

+ 17
- 4
apps/files/src/views/Navigation.vue View File

</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>


<!-- Settings toggle -->
<!-- Non-scrollable navigation bottom elements -->
<template #footer> <template #footer>
<ul class="app-navigation-entry__settings"> <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')" :title="t('files', 'Files settings')"
data-cy-files-navigation-settings-button data-cy-files-navigation-settings-button
@click.prevent.stop="openSettings"> @click.prevent.stop="openSettings">
<script> <script>
import { emit, subscribe } from '@nextcloud/event-bus' import { emit, subscribe } from '@nextcloud/event-bus'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { translate } from '@nextcloud/l10n'

import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import Cog from 'vue-material-design-icons/Cog.vue' import Cog from 'vue-material-design-icons/Cog.vue'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'


import logger from '../logger.js' import logger from '../logger.js'
import Navigation from '../services/Navigation.ts' import Navigation from '../services/Navigation.ts'
import NavigationQuota from '../components/NavigationQuota.vue'
import SettingsModal from './Settings.vue' import SettingsModal from './Settings.vue'


import { translate } from '@nextcloud/l10n'

export default { export default {
name: 'Navigation', name: 'Navigation',


NcAppNavigation, NcAppNavigation,
NcAppNavigationItem, NcAppNavigationItem,
SettingsModal, SettingsModal,
NavigationQuota,
}, },


props: { props: {
currentViewId() { currentViewId() {
return this.$route?.params?.view || 'files' return this.$route?.params?.view || 'files'
}, },

/** @return {Navigation} */
currentView() { currentView() {
return this.views.find(view => view.id === this.currentViewId) return this.views.find(view => view.id === this.currentViewId)
}, },
views() { views() {
return this.Navigation.views return this.Navigation.views
}, },

/** @return {Navigation[]} */
parentViews() { parentViews() {
return this.views return this.views
// filter child views // filter child views
return a.order - b.order return a.order - b.order
}) })
}, },

/** @return {Navigation[]} */
childViews() { childViews() {
return this.views return this.views
// filter parent views // filter parent views


/** /**
* Generate the route to a view * Generate the route to a view
*
* @param {Navigation} view the view to toggle * @param {Navigation} view the view to toggle
*/ */
generateToNavigation(view) { generateToNavigation(view) {

+ 7
- 7
apps/files/src/views/Sidebar.vue View File

return OCA && 'SystemTags' in OCA return OCA && 'SystemTags' in OCA
}, },
}, },
created() {
window.addEventListener('resize', this.handleWindowResize)
this.handleWindowResize()
},
beforeDestroy() {
window.removeEventListener('resize', this.handleWindowResize)
},


methods: { methods: {
/** /**
this.hasLowHeight = document.documentElement.clientHeight < 1024 this.hasLowHeight = document.documentElement.clientHeight < 1024
}, },
}, },
created() {
window.addEventListener('resize', this.handleWindowResize)
this.handleWindowResize()
},
beforeDestroy() {
window.removeEventListener('resize', this.handleWindowResize)
},
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

+ 0
- 44
apps/files/templates/appnavigation.php View File

$pinned = NavigationListElements($item, $l, $pinned); $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> </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> </div>





+ 4
- 8
apps/files/tests/Controller/ViewControllerTest.php View File



public function testIndexWithRegularBrowser() { public function testIndexWithRegularBrowser() {
$this->viewController $this->viewController
->expects($this->once())
->expects($this->any())
->method('getStorageInfo') ->method('getStorageInfo')
->willReturn([ ->willReturn([
'used' => 123, 'used' => 123,
]); ]);


$this->config $this->config
->expects($this->any())
->method('getAppValue')
->willReturnArgument(2);
->expects($this->any())
->method('getAppValue')
->willReturnArgument(2);
$this->shareManager->method('shareApiAllowLinks') $this->shareManager->method('shareApiAllowLinks')
->willReturn(true); ->willReturn(true);


$nav = new Template('files', 'appnavigation'); $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', [ $nav->assign('navigationItems', [
'files' => [ 'files' => [
'id' => 'files', 'id' => 'files',

+ 14
- 0
cypress.config.ts View File

process.env.npm_package_version = '1.0.0' process.env.npm_package_version = '1.0.0'
process.env.NODE_ENV = 'development' 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') const config = require('@nextcloud/webpack-vue-config')
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,

+ 8
- 3
cypress/dockerNode.ts View File

* *
*/ */
/* eslint-disable no-console */ /* 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 Docker from 'dockerode'
import waitOn from 'wait-on' import waitOn from 'wait-on'
* *
* @param {string} branch the branch of your current work * @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 { try {
// Pulling images // Pulling images
// https://github.com/apocas/dockerode/issues/357 // https://github.com/apocas/dockerode/issues/357
docker.modem.followProgress(stream, onFinished) docker.modem.followProgress(stream, onFinished)


/**
*
* @param err
*/
function onFinished(err) { function onFinished(err) {
if (!err) { if (!err) {
resolve(true) resolve(true)
}, },
Env: [ Env: [
`BRANCH=${branch}`, `BRANCH=${branch}`,
]
],
}) })
await container.start() await container.start()



+ 3
- 2
cypress/e2e/theming/admin-settings.cy.ts View File

* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
/* eslint-disable n/no-unpublished-import */
import { User } from '@nextcloud/cypress' import { User } from '@nextcloud/cypress'
import { colord } from 'colord' import { colord } from 'colord'


cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor')


pickRandomColor('[data-admin-theming-setting-primary-color-picker]') pickRandomColor('[data-admin-theming-setting-primary-color-picker]')
.then(color => selectedColor = color)
.then(color => { selectedColor = color })


cy.wait('@setColor') cy.wait('@setColor')
cy.waitUntil(() => validateBodyThemingCss(selectedColor, defaultBackground)) cy.waitUntil(() => validateBodyThemingCss(selectedColor, defaultBackground))
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor') cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor')


pickRandomColor('[data-admin-theming-setting-primary-color-picker]') pickRandomColor('[data-admin-theming-setting-primary-color-picker]')
.then(color => selectedColor = color)
.then(color => { selectedColor = color })


cy.wait('@setColor') cy.wait('@setColor')
cy.waitUntil(() => cy.window().then((win) => { cy.waitUntil(() => cy.window().then((win) => {

+ 2
- 2
cypress/e2e/theming/themingUtils.ts View File

cy.get(pickerSelector).click() cy.get(pickerSelector).click()


// Return selected colour // 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 => { .click().then(colorElement => {
const selectedColor = colorElement.css('background-color') const selectedColor = colorElement.css('background-color')
return selectedColor return selectedColor
}) })
}
}

+ 2
- 2
cypress/e2e/theming/user-background.cy.ts View File

*/ */
import type { User } from '@nextcloud/cypress' import type { User } from '@nextcloud/cypress'


import { pickRandomColor, validateBodyThemingCss } from './themingUtils'

const defaultPrimary = '#006aa3' const defaultPrimary = '#006aa3'
const defaultBackground = 'kamil-porembinski-clouds.jpg' const defaultBackground = 'kamil-porembinski-clouds.jpg'


import { pickRandomColor, validateBodyThemingCss } from './themingUtils'

describe('User default background settings', function() { describe('User default background settings', function() {
before(function() { before(function() {
cy.createRandomUser().then((user: User) => { cy.createRandomUser().then((user: User) => {

+ 4
- 4
cypress/support/commands.ts View File

* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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 axios from '@nextcloud/axios'
import { addCommands, User } from '@nextcloud/cypress' import { addCommands, User } from '@nextcloud/cypress'
import { basename } from 'path' import { basename } from 'path'
/** /**
* Reset the admin theming entirely * Reset the admin theming entirely
*/ */
Cypress.Commands.add('resetAdminTheming', () => {
Cypress.Commands.add('resetAdminTheming', () => {
const admin = new User('admin', 'admin') const admin = new User('admin', 'admin')


cy.clearCookies() cy.clearCookies()
method: 'POST', method: 'POST',
url: '/index.php/apps/theming/ajax/undoAllChanges', url: '/index.php/apps/theming/ajax/undoAllChanges',
headers: { headers: {
'requesttoken': requestToken,
requesttoken: requestToken,
}, },
}) })
}) })
method: 'POST', method: 'POST',
url: '/apps/theming/background/default', url: '/apps/theming/background/default',
headers: { headers: {
'requesttoken': requestToken,
requesttoken: requestToken,
}, },
}) })
}) })

+ 28
- 6
cypress/support/component.ts View File

*/ */
import { mount } from 'cypress/vue2' 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 { declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress { 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')
})
})

+ 1
- 1
cypress/support/e2e.ts View File

* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
import './commands'
import './commands'

+ 2
- 2
dist/core-common.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/core-common.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/files-main.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/files-main.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/files-sidebar.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/files-sidebar.js.map
File diff suppressed because it is too large
View File


+ 6
- 1
lib/private/legacy/OC_Helper.php View File

// return storage info without adding mount points // return storage info without adding mount points
$includeExtStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); $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'); $cacheKey = $fullPath. '::' . ($includeMountPoints ? 'include' : 'exclude');
if ($useCache) { if ($useCache) {
$cached = $memcache->get($cacheKey); $cached = $memcache->get($cacheKey);

+ 16
- 2
package-lock.json View File

"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"colord": "^2.9.3", "colord": "^2.9.3",
"core-js": "^3.24.0", "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", "debounce": "^1.2.1",
"dompurify": "^2.3.6", "dompurify": "^2.3.6",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"snap.js": "^2.0.9", "snap.js": "^2.0.9",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"strengthify": "github:nextcloud/strengthify#0.5.9", "strengthify": "github:nextcloud/strengthify#0.5.9",
"throttle-debounce": "^5.0.0",
"underscore": "1.13.4", "underscore": "1.13.4",
"url-search-params-polyfill": "^8.1.1", "url-search-params-polyfill": "^8.1.1",
"v-click-outside": "^3.2.0", "v-click-outside": "^3.2.0",
"dev": true, "dev": true,
"peer": 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": { "node_modules/throttleit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
}, },
"davclient.js": { "davclient.js": {
"version": "git+ssh://git@github.com/owncloud/davclient.js.git#1ab200d099a3c2cd2ef919c3a56353ce26865994", "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": { "dayjs": {
"version": "1.11.6", "version": "1.11.6",
"dev": true, "dev": true,
"peer": 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": { "throttleit": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",

+ 2
- 1
package.json View File

"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"colord": "^2.9.3", "colord": "^2.9.3",
"core-js": "^3.24.0", "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", "debounce": "^1.2.1",
"dompurify": "^2.3.6", "dompurify": "^2.3.6",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"snap.js": "^2.0.9", "snap.js": "^2.0.9",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"strengthify": "github:nextcloud/strengthify#0.5.9", "strengthify": "github:nextcloud/strengthify#0.5.9",
"throttle-debounce": "^5.0.0",
"underscore": "1.13.4", "underscore": "1.13.4",
"url-search-params-polyfill": "^8.1.1", "url-search-params-polyfill": "^8.1.1",
"v-click-outside": "^3.2.0", "v-click-outside": "^3.2.0",

Loading…
Cancel
Save