aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src')
-rw-r--r--apps/files/src/components/NavigationQuota.vue153
-rw-r--r--apps/files/src/components/PersonalSettings.vue2
-rw-r--r--apps/files/src/components/SidebarTab.vue4
-rw-r--r--apps/files/src/components/TemplatePreview.vue4
-rw-r--r--apps/files/src/components/TransferOwnershipDialogue.vue6
-rw-r--r--apps/files/src/main-personal-settings.js2
-rw-r--r--apps/files/src/reference-files.js58
-rw-r--r--apps/files/src/services/Navigation.ts2
-rw-r--r--apps/files/src/sidebar.js4
-rw-r--r--apps/files/src/templates.js4
-rw-r--r--apps/files/src/views/FileReferencePickerElement.vue113
-rw-r--r--apps/files/src/views/Navigation.cy.ts109
-rw-r--r--apps/files/src/views/Navigation.vue21
-rw-r--r--apps/files/src/views/ReferenceFileWidget.vue182
-rw-r--r--apps/files/src/views/Settings.vue22
-rw-r--r--apps/files/src/views/Sidebar.vue26
-rw-r--r--apps/files/src/views/TemplatePicker.vue10
17 files changed, 679 insertions, 43 deletions
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 @@
+<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>
diff --git a/apps/files/src/components/PersonalSettings.vue b/apps/files/src/components/PersonalSettings.vue
index 1431ae4053a..5f5dc31eafb 100644
--- a/apps/files/src/components/PersonalSettings.vue
+++ b/apps/files/src/components/PersonalSettings.vue
@@ -27,7 +27,7 @@
</template>
<script>
-import TransferOwnershipDialogue from './TransferOwnershipDialogue'
+import TransferOwnershipDialogue from './TransferOwnershipDialogue.vue'
export default {
name: 'PersonalSettings',
diff --git a/apps/files/src/components/SidebarTab.vue b/apps/files/src/components/SidebarTab.vue
index ac3cfba7d02..0a6432b47b6 100644
--- a/apps/files/src/components/SidebarTab.vue
+++ b/apps/files/src/components/SidebarTab.vue
@@ -39,8 +39,8 @@
</template>
<script>
-import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent'
+import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
export default {
name: 'SidebarTab',
diff --git a/apps/files/src/components/TemplatePreview.vue b/apps/files/src/components/TemplatePreview.vue
index ad152af9ea3..832e4d9dda2 100644
--- a/apps/files/src/components/TemplatePreview.vue
+++ b/apps/files/src/components/TemplatePreview.vue
@@ -48,8 +48,8 @@
<script>
import { generateUrl } from '@nextcloud/router'
-import { encodeFilePath } from '../utils/fileUtils'
-import { getToken, isPublic } from '../utils/davUtils'
+import { encodeFilePath } from '../utils/fileUtils.js'
+import { getToken, isPublic } from '../utils/davUtils.js'
// preview width generation
const previewWidth = 256
diff --git a/apps/files/src/components/TransferOwnershipDialogue.vue b/apps/files/src/components/TransferOwnershipDialogue.vue
index 67840b18829..b14f86e8fe2 100644
--- a/apps/files/src/components/TransferOwnershipDialogue.vue
+++ b/apps/files/src/components/TransferOwnershipDialogue.vue
@@ -70,11 +70,11 @@ import axios from '@nextcloud/axios'
import debounce from 'debounce'
import { generateOcsUrl } from '@nextcloud/router'
import { getFilePickerBuilder, showSuccess } from '@nextcloud/dialogs'
-import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect'
+import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect.js'
import Vue from 'vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import logger from '../logger'
+import logger from '../logger.js'
const picker = getFilePickerBuilder(t('files', 'Choose a file or folder to transfer'))
.setMultiSelect(false)
diff --git a/apps/files/src/main-personal-settings.js b/apps/files/src/main-personal-settings.js
index 1d1942e85bb..502d8e30f26 100644
--- a/apps/files/src/main-personal-settings.js
+++ b/apps/files/src/main-personal-settings.js
@@ -25,7 +25,7 @@
import Vue from 'vue'
import { getRequestToken } from '@nextcloud/auth'
-import PersonalSettings from './components/PersonalSettings'
+import PersonalSettings from './components/PersonalSettings.vue'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())
diff --git a/apps/files/src/reference-files.js b/apps/files/src/reference-files.js
new file mode 100644
index 00000000000..db563200cd0
--- /dev/null
+++ b/apps/files/src/reference-files.js
@@ -0,0 +1,58 @@
+/**
+ * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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/>.
+ */
+
+import Vue from 'vue'
+import { translate as t } from '@nextcloud/l10n'
+
+import { registerWidget, registerCustomPickerElement, NcCustomPickerRenderResult } from '@nextcloud/vue/dist/Components/NcRichText.js'
+
+import FileWidget from './views/ReferenceFileWidget.vue'
+import FileReferencePickerElement from './views/FileReferencePickerElement.vue'
+
+Vue.mixin({
+ methods: {
+ t,
+ },
+})
+
+registerWidget('file', (el, { richObjectType, richObject, accessible }) => {
+ const Widget = Vue.extend(FileWidget)
+ new Widget({
+ propsData: {
+ richObjectType,
+ richObject,
+ accessible,
+ },
+ }).$mount(el)
+})
+
+registerCustomPickerElement('files', (el, { providerId, accessible }) => {
+ const Element = Vue.extend(FileReferencePickerElement)
+ const vueElement = new Element({
+ propsData: {
+ providerId,
+ accessible,
+ },
+ }).$mount(el)
+ return new NcCustomPickerRenderResult(vueElement.$el, vueElement)
+}, (el, renderResult) => {
+ renderResult.object.$destroy()
+})
diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts
index e3286c79a88..9efed538825 100644
--- a/apps/files/src/services/Navigation.ts
+++ b/apps/files/src/services/Navigation.ts
@@ -22,7 +22,7 @@
import type Node from '@nextcloud/files/dist/files/node'
import isSvg from 'is-svg'
-import logger from '../logger'
+import logger from '../logger.js'
export interface Column {
/** Unique column ID */
diff --git a/apps/files/src/sidebar.js b/apps/files/src/sidebar.js
index 58b798ed0e7..3cdb8c4fb0b 100644
--- a/apps/files/src/sidebar.js
+++ b/apps/files/src/sidebar.js
@@ -24,8 +24,8 @@ import Vue from 'vue'
import { translate as t } from '@nextcloud/l10n'
import SidebarView from './views/Sidebar.vue'
-import Sidebar from './services/Sidebar'
-import Tab from './models/Tab'
+import Sidebar from './services/Sidebar.js'
+import Tab from './models/Tab.js'
Vue.prototype.t = t
diff --git a/apps/files/src/templates.js b/apps/files/src/templates.js
index 7f7ebbf2dcc..3a4f0133f94 100644
--- a/apps/files/src/templates.js
+++ b/apps/files/src/templates.js
@@ -25,11 +25,11 @@ import { getLoggerBuilder } from '@nextcloud/logger'
import { loadState } from '@nextcloud/initial-state'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
-import { getCurrentDirectory } from './utils/davUtils'
+import { getCurrentDirectory } from './utils/davUtils.js'
import axios from '@nextcloud/axios'
import Vue from 'vue'
-import TemplatePickerView from './views/TemplatePicker'
+import TemplatePickerView from './views/TemplatePicker.vue'
import { showError } from '@nextcloud/dialogs'
// Set up logger
diff --git a/apps/files/src/views/FileReferencePickerElement.vue b/apps/files/src/views/FileReferencePickerElement.vue
new file mode 100644
index 00000000000..543dba3350d
--- /dev/null
+++ b/apps/files/src/views/FileReferencePickerElement.vue
@@ -0,0 +1,113 @@
+<!--
+ - @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @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/>.
+ -->
+
+<template>
+ <div ref="picker" class="reference-file-picker" />
+</template>
+
+<script>
+import { FilePickerType } from '@nextcloud/dialogs'
+import { generateUrl } from '@nextcloud/router'
+export default {
+ name: 'FileReferencePickerElement',
+ components: {
+ },
+ props: {
+ providerId: {
+ type: String,
+ required: true,
+ },
+ accessible: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ mounted() {
+ this.openFilePicker()
+ window.addEventListener('click', this.onWindowClick)
+ },
+ beforeDestroy() {
+ window.removeEventListener('click', this.onWindowClick)
+ },
+ methods: {
+ onWindowClick(e) {
+ if (e.target.tagName === 'A' && e.target.classList.contains('oc-dialog-close')) {
+ this.$emit('cancel')
+ }
+ },
+ async openFilePicker() {
+ OC.dialogs.filepicker(
+ t('files', 'Select file or folder to link to'),
+ (file) => {
+ const client = OC.Files.getClient()
+ client.getFileInfo(file).then((_status, fileInfo) => {
+ this.submit(fileInfo.id)
+ })
+ },
+ false, // multiselect
+ [], // mime filter
+ false, // modal
+ FilePickerType.Choose, // type
+ '',
+ {
+ target: this.$refs.picker,
+ },
+ )
+ },
+ submit(fileId) {
+ const fileLink = window.location.protocol + '//' + window.location.host
+ + generateUrl('/f/{fileId}', { fileId })
+ this.$emit('submit', fileLink)
+ },
+ },
+}
+</script>
+
+<style scoped lang="scss">
+.reference-file-picker {
+ flex-grow: 1;
+ margin-top: 44px;
+
+ &:deep(.oc-dialog) {
+ transform: none !important;
+ box-shadow: none !important;
+ flex-grow: 1 !important;
+ position: static !important;
+ width: 100% !important;
+ height: auto !important;
+ padding: 0 !important;
+ max-width: initial;
+
+ .oc-dialog-close {
+ display: none;
+ }
+
+ .oc-dialog-buttonrow.onebutton.aside {
+ position: absolute;
+ padding: 12px 32px;
+ }
+
+ .oc-dialog-content {
+ max-width: 100% !important;
+ }
+ }
+}
+</style>
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 05fc7cdacd2..d9fdfa7fe02 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -42,9 +42,13 @@
</NcAppNavigationItem>
</template>
- <!-- Settings toggle -->
+ <!-- Non-scrollable navigation bottom elements -->
<template #footer>
<ul class="app-navigation-entry__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
@@ -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,9 +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'
+import { setPageHeading } from '../../../../core/src/OCP/accessibility.js'
export default {
name: 'Navigation',
@@ -83,6 +89,7 @@ export default {
NcAppNavigation,
NcAppNavigationItem,
SettingsModal,
+ NavigationQuota,
},
props: {
@@ -103,6 +110,8 @@ export default {
currentViewId() {
return this.$route?.params?.view || 'files'
},
+
+ /** @return {Navigation} */
currentView() {
return this.views.find(view => view.id === this.currentViewId)
},
@@ -111,6 +120,8 @@ export default {
views() {
return this.Navigation.views
},
+
+ /** @return {Navigation[]} */
parentViews() {
return this.views
// filter child views
@@ -120,6 +131,8 @@ export default {
return a.order - b.order
})
},
+
+ /** @return {Navigation[]} */
childViews() {
return this.views
// filter parent views
@@ -179,6 +192,7 @@ export default {
}
this.Navigation.setActive(view)
+ setPageHeading(view.name)
emit('files:navigation:changed', view)
},
@@ -213,6 +227,7 @@ export default {
/**
* Generate the route to a view
+ *
* @param {Navigation} view the view to toggle
*/
generateToNavigation(view) {
diff --git a/apps/files/src/views/ReferenceFileWidget.vue b/apps/files/src/views/ReferenceFileWidget.vue
new file mode 100644
index 00000000000..f0ac7007312
--- /dev/null
+++ b/apps/files/src/views/ReferenceFileWidget.vue
@@ -0,0 +1,182 @@
+<!--
+ - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @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/>.
+ -->
+
+<template>
+ <div v-if="!accessible" class="widget-file widget-file--no-access">
+ <div class="widget-file--image widget-file--image--icon icon-folder" />
+ <div class="widget-file--details">
+ <p class="widget-file--title">
+ {{ t('files', 'File cannot be accessed') }}
+ </p>
+ <p class="widget-file--description">
+ {{ t('files', 'You might not have have permissions to view it, ask the sender to share it') }}
+ </p>
+ </div>
+ </div>
+ <a v-else
+ class="widget-file"
+ :href="richObject.link"
+ @click.prevent="navigate">
+ <div class="widget-file--image" :class="filePreviewClass" :style="filePreview" />
+ <div class="widget-file--details">
+ <p class="widget-file--title">{{ richObject.name }}</p>
+ <p class="widget-file--description">{{ fileSize }}<br>{{ fileMtime }}</p>
+ <p class="widget-file--link">{{ filePath }}</p>
+ </div>
+ </a>
+</template>
+<script>
+import { generateUrl } from '@nextcloud/router'
+import path from 'path'
+
+export default {
+ name: 'ReferenceFileWidget',
+ props: {
+ richObject: {
+ type: Object,
+ required: true,
+ },
+ accessible: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ previewUrl: window.OC.MimeType.getIconUrl(this.richObject.mimetype),
+ }
+ },
+ computed: {
+ fileSize() {
+ return window.OC.Util.humanFileSize(this.richObject.size)
+ },
+ fileMtime() {
+ return window.OC.Util.relativeModifiedDate(this.richObject.mtime * 1000)
+ },
+ filePath() {
+ return path.dirname(this.richObject.path)
+ },
+ filePreview() {
+ if (this.previewUrl) {
+ return {
+ backgroundImage: 'url(' + this.previewUrl + ')',
+ }
+ }
+
+ return {
+ backgroundImage: 'url(' + window.OC.MimeType.getIconUrl(this.richObject.mimetype) + ')',
+ }
+
+ },
+ filePreviewClass() {
+ if (this.previewUrl) {
+ return 'widget-file--image--preview'
+ }
+ return 'widget-file--image--icon'
+
+ },
+ },
+ mounted() {
+ if (this.richObject['preview-available']) {
+ const previewUrl = generateUrl('/core/preview?fileId={fileId}&x=250&y=250', {
+ fileId: this.richObject.id,
+ })
+ const img = new Image()
+ img.onload = () => {
+ this.previewUrl = previewUrl
+ }
+ img.onerror = err => {
+ console.error('could not load recommendation preview', err)
+ }
+ img.src = previewUrl
+ }
+ },
+ methods: {
+ navigate() {
+ if (OCA.Viewer && OCA.Viewer.mimetypes.indexOf(this.richObject.mimetype) !== -1) {
+ OCA.Viewer.open({ path: this.richObject.path })
+ return
+ }
+ window.location = generateUrl('/f/' + this.id)
+ },
+ },
+}
+</script>
+<style lang="scss" scoped>
+.widget-file {
+ display: flex;
+ flex-grow: 1;
+ color: var(--color-main-text) !important;
+ text-decoration: none !important;
+
+ &--image {
+ min-width: 40%;
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+
+ &.widget-file--image--icon {
+ min-width: 88px;
+ background-size: 44px;
+ }
+ }
+
+ &--title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: bold;
+ }
+
+ &--details {
+ padding: 12px;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+
+ p {
+ margin: 0;
+ padding: 0;
+ }
+ }
+
+ &--description {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+ -webkit-box-orient: vertical;
+ }
+
+ &--link {
+ color: var(--color-text-maxcontrast);
+ }
+
+ &.widget-file--no-access {
+ padding: 12px;
+
+ .widget-file--details {
+ padding: 0;
+ }
+ }
+}
+</style>
diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue
index 9a63fea4924..9a117b70e22 100644
--- a/apps/files/src/views/Settings.vue
+++ b/apps/files/src/views/Settings.vue
@@ -46,7 +46,7 @@
</NcAppSettingsSection>
<!-- Webdav URL-->
- <NcAppSettingsSection id="webdav" :title="t('files', 'Webdav')">
+ <NcAppSettingsSection id="webdav" :title="t('files', 'WebDAV')">
<NcInputField id="webdav-url-input"
:show-trailing-button="true"
:success="webdavUrlCopied"
@@ -61,10 +61,19 @@
</template>
</NcInputField>
<em>
- <a :href="webdavDocs" target="_blank" rel="noreferrer noopener">
+ <a class="setting-link"
+ :href="webdavDocs"
+ target="_blank"
+ rel="noreferrer noopener">
{{ t('files', 'Use this address to access your Files via WebDAV') }} ↗
</a>
</em>
+ <br>
+ <em>
+ <a class="setting-link" :href="appPasswordUrl">
+ {{ t('files', 'If you have enabled 2FA, you must create and use a new app password by clicking here.') }} ↗
+ </a>
+ </em>
</NcAppSettingsSection>
</NcAppSettingsDialog>
</template>
@@ -74,7 +83,7 @@ import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDia
import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import Clipboard from 'vue-material-design-icons/Clipboard.vue'
-import NcInputField from '@nextcloud/vue/dist/Components/NcInputField'
+import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
import Setting from '../components/Setting.vue'
import { emit } from '@nextcloud/event-bus'
@@ -119,6 +128,7 @@ export default {
// Webdav infos
webdavUrl: generateRemoteUrl('dav/files/' + encodeURIComponent(getCurrentUser()?.uid)),
webdavDocs: 'https://docs.nextcloud.com/server/stable/go.php?to=user-webdav',
+ appPasswordUrl: generateUrl('/settings/user/security#generate-app-token-section'),
webdavUrlCopied: false,
}
},
@@ -156,7 +166,7 @@ export default {
await navigator.clipboard.writeText(this.webdavUrl)
this.webdavUrlCopied = true
- showSuccess(t('files', 'Webdav URL copied to clipboard'))
+ showSuccess(t('files', 'WebDAV URL copied to clipboard'))
setTimeout(() => {
this.webdavUrlCopied = false
}, 5000)
@@ -168,5 +178,7 @@ export default {
</script>
<style lang="scss" scoped>
-
+.setting-link:hover {
+ text-decoration: underline;
+}
</style>
diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue
index 1fb60f7fc39..5c3967b1c93 100644
--- a/apps/files/src/views/Sidebar.vue
+++ b/apps/files/src/views/Sidebar.vue
@@ -89,13 +89,13 @@ import { emit } from '@nextcloud/event-bus'
import moment from '@nextcloud/moment'
import { Type as ShareTypes } from '@nextcloud/sharing'
-import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent'
+import NcAppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
+import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import FileInfo from '../services/FileInfo'
-import SidebarTab from '../components/SidebarTab'
-import LegacyView from '../components/LegacyView'
+import FileInfo from '../services/FileInfo.js'
+import SidebarTab from '../components/SidebarTab.vue'
+import LegacyView from '../components/LegacyView.vue'
export default {
name: 'Sidebar',
@@ -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>
diff --git a/apps/files/src/views/TemplatePicker.vue b/apps/files/src/views/TemplatePicker.vue
index 33b925aa2ed..79264d56074 100644
--- a/apps/files/src/views/TemplatePicker.vue
+++ b/apps/files/src/views/TemplatePicker.vue
@@ -66,12 +66,12 @@
<script>
import { normalize } from 'path'
import { showError } from '@nextcloud/dialogs'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent'
-import NcModal from '@nextcloud/vue/dist/Components/NcModal'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
-import { getCurrentDirectory } from '../utils/davUtils'
-import { createFromTemplate, getTemplates } from '../services/Templates'
-import TemplatePreview from '../components/TemplatePreview'
+import { getCurrentDirectory } from '../utils/davUtils.js'
+import { createFromTemplate, getTemplates } from '../services/Templates.js'
+import TemplatePreview from '../components/TemplatePreview.vue'
const border = 2
const margin = 8