Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v26.0.0beta1
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. |
'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}', |
'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', |
'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', |
<?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' | |||||
], | |||||
]); | |||||
} | |||||
} | |||||
} |
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 | ||||
* | * |
* @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()); | ||||
<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> |
/* 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 | |||||
}) | |||||
}) |
</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) { |
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> |
$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> | ||||
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', |
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$/, |
* | * | ||||
*/ | */ | ||||
/* 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() | ||||
* 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) => { |
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 | ||||
}) | }) | ||||
} | |||||
} |
*/ | */ | ||||
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) => { |
* 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, | |||||
}, | }, | ||||
}) | }) | ||||
}) | }) |
*/ | */ | ||||
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') | |||||
}) | |||||
}) |
* 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' |
// 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); |
"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", |
"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", |