mirror of https://github.com/nextcloud/server
feat(files): Quota in navigation
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
9af7ee8d11
commit
b9906fb21e
|
@ -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'
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue