diff --git a/.eslintrc.js b/.eslintrc.js index f4333f93d21..812fec4c009 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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. diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 9c45668333b..dabbeab978e 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -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}', diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 0f6c2caf4f2..ef3480081e0 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -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', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 28e48b9919e..4f7872e39df 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.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', diff --git a/apps/files/lib/Controller/AjaxController.php b/apps/files/lib/Controller/AjaxController.php deleted file mode 100644 index cd26ab7a6f8..00000000000 --- a/apps/files/lib/Controller/AjaxController.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * @author Roeland Jago Douma - * - * @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 . - * - */ -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' - ], - ]); - } - } -} diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index f2329fc384b..604cf9a3c64 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -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 * diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 0594b63f56b..b607764e602 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -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()); diff --git a/apps/files/src/components/NavigationQuota.vue b/apps/files/src/components/NavigationQuota.vue new file mode 100644 index 00000000000..bfcbaea3776 --- /dev/null +++ b/apps/files/src/components/NavigationQuota.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts index 65c5d8938a9..c8b0f07dea1 100644 --- a/apps/files/src/views/Navigation.cy.ts +++ b/apps/files/src/views/Navigation.cy.ts @@ -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 + }) +}) diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 074bee0b010..040e1482e32 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -42,10 +42,14 @@ - +