diff options
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/NavigationQuota.vue | 153 | ||||
-rw-r--r-- | apps/files/src/components/PersonalSettings.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/SidebarTab.vue | 4 | ||||
-rw-r--r-- | apps/files/src/components/TemplatePreview.vue | 4 | ||||
-rw-r--r-- | apps/files/src/components/TransferOwnershipDialogue.vue | 6 | ||||
-rw-r--r-- | apps/files/src/main-personal-settings.js | 2 | ||||
-rw-r--r-- | apps/files/src/reference-files.js | 58 | ||||
-rw-r--r-- | apps/files/src/services/Navigation.ts | 2 | ||||
-rw-r--r-- | apps/files/src/sidebar.js | 4 | ||||
-rw-r--r-- | apps/files/src/templates.js | 4 | ||||
-rw-r--r-- | apps/files/src/views/FileReferencePickerElement.vue | 113 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.cy.ts | 109 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 21 | ||||
-rw-r--r-- | apps/files/src/views/ReferenceFileWidget.vue | 182 | ||||
-rw-r--r-- | apps/files/src/views/Settings.vue | 22 | ||||
-rw-r--r-- | apps/files/src/views/Sidebar.vue | 26 | ||||
-rw-r--r-- | apps/files/src/views/TemplatePicker.vue | 10 |
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 |